tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 404 """ 405 try: 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 except Exception as e: 414 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 415 416 return {} 417 418 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 419 """ 420 Send GET or POST request to broker server and receive JSON object. 421 422 self.header: must be defining with dictionary of headers. 423 self.body: if define then used as request body. None by default. 424 self.timeout: global request timeout, 15 seconds by default. 425 :param url: url with REST request. 426 :param reqType: send "GET" or "POST" request. "GET" by default. 427 :param retry: how many times retry after first request if an 5xx server errors occurred. 428 :param pause: sleep time in seconds between retries. 429 :return: response JSON (dictionary) from broker. 430 """ 431 if reqType.upper() not in ("GET", "POST"): 432 uLogger.error("You can define request type: `GET` or `POST`!") 433 raise Exception("Incorrect value") 434 435 if self.moreDebug: 436 uLogger.debug("Request parameters:") 437 uLogger.debug(" - REST API URL: {}".format(url)) 438 uLogger.debug(" - request type: {}".format(reqType)) 439 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 440 uLogger.debug(" - body:\n{}".format(self.body)) 441 442 # fast hack to avoid all operations with some tickers/FIGI 443 responseJSON = {} 444 oK = True 445 for item in self.exclude: 446 if item in url: 447 if self.moreDebug: 448 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 449 450 oK = False 451 break 452 453 if oK: 454 with self.__lock: # acquire the mutex lock 455 counter = 0 456 response = None 457 errMsg = "" 458 459 while not response and counter <= retry: 460 if reqType == "GET": 461 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 462 463 if reqType == "POST": 464 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 465 466 if self.moreDebug: 467 uLogger.debug("Response:") 468 uLogger.debug(" - status code: {}".format(response.status_code)) 469 uLogger.debug(" - reason: {}".format(response.reason)) 470 uLogger.debug(" - body length: {}".format(len(response.text))) 471 uLogger.debug(" - headers:\n{}".format(response.headers)) 472 473 # Server returns some headers: 474 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 475 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 476 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 477 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 478 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 479 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 480 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 481 sleep(rateLimitWait) 482 483 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 484 if 400 <= response.status_code < 500: 485 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 486 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 487 488 if "code" in response.text and "message" in response.text: 489 msgDict = self._ParseJSON(rawData=response.text) 490 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 491 492 counter = retry + 1 # do not retry for 4xx errors 493 494 if 500 <= response.status_code < 600: 495 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 496 uLogger.debug(" - not oK, {}".format(errMsg)) 497 498 if "code" in response.text and "message" in response.text: 499 errMsgDict = self._ParseJSON(rawData=response.text) 500 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 501 502 counter += 1 503 504 if counter <= retry: 505 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 506 sleep(pause) 507 508 responseJSON = self._ParseJSON(rawData=response.text) 509 510 if errMsg: 511 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 512 uLogger.error(" - not oK, {}".format(errMsg)) 513 514 return responseJSON 515 516 def _IUpdater(self, iType: str) -> tuple: 517 """ 518 Request instrument by type from server. See available API methods for instruments: 519 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 520 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 521 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 522 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 523 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 524 525 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 526 :return: tuple with iType name and list of available instruments of current type for defined user token. 527 """ 528 result = [] 529 530 if iType in TKS_INSTRUMENTS: 531 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 532 533 # all instruments have the same body in API v2 requests: 534 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 535 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 536 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 537 538 return iType, result 539 540 def _IWrapper(self, kwargs): 541 """ 542 Wrapper runs instrument's update method `_IUpdater()`. 543 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 544 """ 545 return self._IUpdater(**kwargs) 546 547 def Listing(self) -> dict: 548 """ 549 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 550 551 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 552 """ 553 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 554 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 555 556 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 557 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 558 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 559 560 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 561 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 562 poolUpdater.close() # close the thread pool 563 poolUpdater.join() # wait a moment until all data returns from threads 564 565 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 566 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 567 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 568 569 # calculate minimum price increment (step) for all instruments and set up instrument's type: 570 for iType in iList.keys(): 571 for ticker in iList[iType]: 572 iList[iType][ticker]["type"] = iType 573 574 if "minPriceIncrement" in iList[iType][ticker].keys(): 575 iList[iType][ticker]["step"] = NanoToFloat( 576 iList[iType][ticker]["minPriceIncrement"]["units"], 577 iList[iType][ticker]["minPriceIncrement"]["nano"], 578 ) 579 580 else: 581 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 582 583 return iList 584 585 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 586 """ 587 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 588 589 See also: `DumpInstruments()`, `Listing()`. 590 591 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 592 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 593 """ 594 if self.iListDumpFile is None or not self.iListDumpFile: 595 uLogger.error("Output name of dump file must be defined!") 596 raise Exception("Filename required") 597 598 if not self.iList or forceUpdate: 599 self.iList = self.Listing() 600 601 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 602 603 # Save as XLSX with separated sheets for every type of instruments: 604 with pd.ExcelWriter( 605 path=xlsxDumpFile, 606 date_format=TKS_DATE_FORMAT, 607 datetime_format=TKS_DATE_TIME_FORMAT, 608 mode="w", 609 ) as writer: 610 for iType in TKS_INSTRUMENTS: 611 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 612 df = df[sorted(df)] # sorted by column names 613 df = df.applymap( 614 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 615 na_action="ignore", 616 ) # converting numbers from nano-type to float in every cell 617 df.to_excel( 618 writer, 619 sheet_name=iType, 620 encoding="UTF-8", 621 freeze_panes=(1, 1), 622 ) # saving as XLSX-file with freeze first row and column as headers 623 624 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 625 626 def DumpInstruments(self, forceUpdate: bool = True) -> str: 627 """ 628 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 629 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 630 631 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 635 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 645 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 646 fH.write(jsonDump) 647 648 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 649 650 return jsonDump 651 652 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 653 """ 654 Show information about one instrument defined by json data and prints it in Markdown format. 655 656 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 657 658 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 659 :param show: if `True` then also printing information about instrument and its current price. 660 :return: multilines text in Markdown format with information about one instrument. 661 """ 662 splitLine = "| | |\n" 663 infoText = "" 664 665 if iJSON is not None and iJSON and isinstance(iJSON, dict): 666 info = [ 667 "# Main information\n\n", 668 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 669 "| Parameters | Values |\n", 670 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 671 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 672 "| Full name: | {:<54} |\n".format(iJSON["name"]), 673 ] 674 675 if "sector" in iJSON.keys() and iJSON["sector"]: 676 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 677 678 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 679 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 680 681 info.extend([ 682 splitLine, 683 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 684 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 685 ]) 686 687 if "isin" in iJSON.keys() and iJSON["isin"]: 688 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 689 690 if "classCode" in iJSON.keys(): 691 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 692 693 info.extend([ 694 splitLine, 695 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 696 splitLine, 697 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 698 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 699 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 700 ]) 701 702 if iJSON["figi"]: 703 self._figi = iJSON["figi"] 704 iJSON = iJSON | self.RequestTradingStatus() 705 706 info.extend([ 707 splitLine, 708 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 709 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 710 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 711 ]) 712 713 info.append(splitLine) 714 715 if "type" in iJSON.keys() and iJSON["type"]: 716 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 717 718 if "shareType" in iJSON.keys() and iJSON["shareType"]: 719 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 720 721 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 722 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 723 724 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 725 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 726 727 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 728 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 729 730 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 731 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 732 733 if "focusType" in iJSON.keys() and iJSON["focusType"]: 734 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 735 736 if "assetType" in iJSON.keys() and iJSON["assetType"]: 737 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 738 739 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 740 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 741 742 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 743 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 744 745 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 746 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 747 748 if "currency" in iJSON.keys(): 749 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 750 751 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 752 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 753 754 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 755 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 756 757 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 758 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 759 760 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 761 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 762 763 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 764 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 765 766 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 767 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 768 769 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 770 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 771 772 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 773 info.append("| Perpetual bond: | Yes |\n") 774 775 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 776 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 777 778 iExt = None 779 if iJSON["type"] == "Bonds": 780 info.extend([ 781 splitLine, 782 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 783 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 784 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 785 iJSON["nominal"]["currency"], 786 )), 787 ]) 788 789 if "floatingCouponFlag" in iJSON.keys(): 790 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 791 792 if "amortizationFlag" in iJSON.keys(): 793 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 794 795 info.append(splitLine) 796 797 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 798 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 799 800 if iJSON["figi"]: 801 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 802 803 info.extend([ 804 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 805 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 806 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 807 ]) 808 809 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 810 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 811 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 812 iJSON["aciValue"]["currency"] 813 ))) 814 815 if "currentPrice" in iJSON.keys(): 816 info.append(splitLine) 817 818 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 819 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 820 821 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 822 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 823 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 824 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 825 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 826 827 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 828 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 829 830 info.extend([ 831 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 832 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 833 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 834 )), 835 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 836 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 837 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 838 )), 839 "| Changes between last deal price and last close | {:<54} |\n".format( 840 "{:.2f}%{}".format( 841 iJSON["currentPrice"]["changes"], 842 " ({}{:.2f} {})".format( 843 "+" if bondChangesDelta > 0 else "", 844 bondChangesDelta, 845 aciCurrency 846 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 847 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 848 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 849 currency 850 ), 851 ) 852 ), 853 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 854 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 855 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 856 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 859 )), 860 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 861 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 862 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 863 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 865 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 866 )), 867 ]) 868 869 if "lot" in iJSON.keys(): 870 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 871 872 if "step" in iJSON.keys() and iJSON["step"] != 0: 873 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 874 875 # Add bond payment calendar: 876 if iJSON["type"] == "Bonds": 877 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 878 info.extend(["\n#", strCalendar]) 879 880 infoText += "".join(info) 881 882 if show: 883 uLogger.info("{}".format(infoText)) 884 885 else: 886 uLogger.debug("{}".format(infoText)) 887 888 if self.infoFile is not None: 889 with open(self.infoFile, "w", encoding="UTF-8") as fH: 890 fH.write(infoText) 891 892 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 893 894 if self.useHTMLReports: 895 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 896 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 897 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 898 899 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 900 901 return infoText 902 903 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 904 """ 905 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 906 907 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 908 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 909 :return: JSON formatted data with information about instrument. 910 """ 911 tickerJSON = {} 912 if self.moreDebug: 913 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 914 915 if not self._ticker: 916 uLogger.warning("self._ticker variable is not be empty!") 917 918 else: 919 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 920 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 921 raise Exception("Instrument not allowed") 922 923 if not self.iList: 924 self.iList = self.Listing() 925 926 if self._ticker in self.iList["Shares"].keys(): 927 tickerJSON = self.iList["Shares"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Currencies"].keys(): 932 tickerJSON = self.iList["Currencies"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Bonds"].keys(): 937 tickerJSON = self.iList["Bonds"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Etfs"].keys(): 942 tickerJSON = self.iList["Etfs"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 945 946 elif self._ticker in self.iList["Futures"].keys(): 947 tickerJSON = self.iList["Futures"][self._ticker] 948 if self.moreDebug: 949 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 950 951 if tickerJSON: 952 self._figi = tickerJSON["figi"] 953 954 if requestPrice: 955 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 956 957 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 958 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 959 960 else: 961 tickerJSON["currentPrice"]["changes"] = 0 962 963 if show: 964 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 965 966 else: 967 if show: 968 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 969 970 return tickerJSON 971 972 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 973 """ 974 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 975 976 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 977 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 978 :return: JSON formatted data with information about instrument. 979 """ 980 figiJSON = {} 981 if self.moreDebug: 982 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 983 984 if not self._figi: 985 uLogger.warning("self._figi variable is not be empty!") 986 987 else: 988 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 989 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 990 raise Exception("Instrument not allowed") 991 992 if not self.iList: 993 self.iList = self.Listing() 994 995 for item in self.iList["Shares"].keys(): 996 if self._figi == self.iList["Shares"][item]["figi"]: 997 figiJSON = self.iList["Shares"][item] 998 999 if self.moreDebug: 1000 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1001 1002 break 1003 1004 if not figiJSON: 1005 for item in self.iList["Currencies"].keys(): 1006 if self._figi == self.iList["Currencies"][item]["figi"]: 1007 figiJSON = self.iList["Currencies"][item] 1008 1009 if self.moreDebug: 1010 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1011 1012 break 1013 1014 if not figiJSON: 1015 for item in self.iList["Bonds"].keys(): 1016 if self._figi == self.iList["Bonds"][item]["figi"]: 1017 figiJSON = self.iList["Bonds"][item] 1018 1019 if self.moreDebug: 1020 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1021 1022 break 1023 1024 if not figiJSON: 1025 for item in self.iList["Etfs"].keys(): 1026 if self._figi == self.iList["Etfs"][item]["figi"]: 1027 figiJSON = self.iList["Etfs"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Futures"].keys(): 1036 if self._figi == self.iList["Futures"][item]["figi"]: 1037 figiJSON = self.iList["Futures"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1041 1042 break 1043 1044 if figiJSON: 1045 self._figi = figiJSON["figi"] 1046 self._ticker = figiJSON["ticker"] 1047 1048 if requestPrice: 1049 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1050 1051 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1052 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1053 1054 else: 1055 figiJSON["currentPrice"]["changes"] = 0 1056 1057 if show: 1058 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1059 1060 else: 1061 if show: 1062 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1063 1064 return figiJSON 1065 1066 def GetCurrentPrices(self, show: bool = True) -> dict: 1067 """ 1068 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1069 `{"buy": [{"price": 1243.8, "quantity": 193}, 1070 {"price": 1244.0, "quantity": 168}, 1071 {"price": 1244.8, "quantity": 5}, 1072 {"price": 1245.0, "quantity": 61}, 1073 {"price": 1245.4, "quantity": 60}], 1074 "sell": [{"price": 1243.6, "quantity": 8}, 1075 {"price": 1242.6, "quantity": 10}, 1076 {"price": 1242.4, "quantity": 18}, 1077 {"price": 1242.2, "quantity": 50}, 1078 {"price": 1242.0, "quantity": 113}], 1079 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1080 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1081 - sell: list of dicts with Buyers prices, 1082 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1083 - quantity: volume value by current price in lots, 1084 - limitUp: current trade session limit price, maximum, 1085 - limitDown: current trade session limit price, minimum, 1086 - lastPrice: last deal price of the instrument, 1087 - closePrice: previous trade session close price of the instrument. 1088 1089 See also: `SearchByTicker()` and `SearchByFIGI()`. 1090 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1091 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1092 1093 :param show: if `True` then print DOM to log and console. 1094 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1095 If an error occurred then returns an empty record: 1096 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1097 """ 1098 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1099 1100 if self.depth < 1: 1101 uLogger.error("Depth of Market (DOM) must be >=1!") 1102 raise Exception("Incorrect value") 1103 1104 if not (self._ticker or self._figi): 1105 uLogger.error("self._ticker or self._figi variables must be defined!") 1106 raise Exception("Ticker or FIGI required") 1107 1108 if self._ticker and not self._figi: 1109 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1110 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1111 1112 if not self._ticker and self._figi: 1113 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1114 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1115 1116 if not self._figi: 1117 uLogger.error("FIGI is not defined!") 1118 raise Exception("Ticker or FIGI required") 1119 1120 else: 1121 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1122 1123 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1124 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1125 self.body = str({"figi": self._figi, "depth": self.depth}) 1126 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1127 1128 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1129 # list of dicts with sellers orders: 1130 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1131 1132 # list of dicts with buyers orders: 1133 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1134 1135 # max price of instrument at this time: 1136 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1137 1138 # min price of instrument at this time: 1139 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1140 1141 # last price of deal with instrument: 1142 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1143 1144 # last close price of instrument: 1145 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1146 1147 else: 1148 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1149 uLogger.debug("Server response: {}".format(pricesResponse)) 1150 1151 if show: 1152 if prices["buy"] or prices["sell"]: 1153 info = [ 1154 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1155 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1156 self._ticker, 1157 self._figi, 1158 self.depth, 1159 ), 1160 "-" * 60, "\n", 1161 " Orders of Buyers | Orders of Sellers\n", 1162 "-" * 60, "\n", 1163 " Sell prices (volumes) | Buy prices (volumes)\n", 1164 "-" * 60, "\n", 1165 ] 1166 1167 if not prices["buy"]: 1168 info.append(" | No orders!\n") 1169 sumBuy = 0 1170 1171 else: 1172 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1173 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1174 for item in maxMinSorted: 1175 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1176 1177 if not prices["sell"]: 1178 info.append("No orders! |\n") 1179 sumSell = 0 1180 1181 else: 1182 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1183 for item in prices["sell"]: 1184 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1185 1186 info.extend([ 1187 "-" * 60, "\n", 1188 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1189 "-" * 60, "\n", 1190 ]) 1191 1192 infoText = "".join(info) 1193 1194 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1195 1196 else: 1197 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1198 1199 return prices 1200 1201 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1202 """ 1203 This method get and show information about all available broker instruments for current user account. 1204 If `instrumentsFile` string is not empty then also save information to this file. 1205 1206 :param show: if `True` then print results to console, if `False` — print only to file. 1207 :return: multi-lines string with all available broker instruments 1208 """ 1209 if not self.iList: 1210 self.iList = self.Listing() 1211 1212 info = [ 1213 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1214 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1215 ] 1216 1217 # add instruments count by type: 1218 for iType in self.iList.keys(): 1219 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1220 1221 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1222 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1223 1224 # generating info tables with all instruments by type: 1225 for iType in self.iList.keys(): 1226 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1227 1228 for instrument in self.iList[iType].keys(): 1229 iName = self.iList[iType][instrument]["name"] # instrument's name 1230 if len(iName) > 57: 1231 iName = "{}...".format(iName[:54]) # right trim for a long string 1232 1233 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1234 self.iList[iType][instrument]["ticker"], 1235 iName, 1236 self.iList[iType][instrument]["figi"], 1237 self.iList[iType][instrument]["currency"], 1238 self.iList[iType][instrument]["lot"], 1239 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1240 )) 1241 1242 infoText = "".join(info) 1243 1244 if show: 1245 uLogger.info(infoText) 1246 1247 if self.instrumentsFile: 1248 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1249 fH.write(infoText) 1250 1251 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1252 1253 if self.useHTMLReports: 1254 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1255 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1256 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1257 1258 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1259 1260 return infoText 1261 1262 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1263 """ 1264 This method search and show information about instruments by part of its ticker, FIGI or name. 1265 If `searchResultsFile` string is not empty then also save information to this file. 1266 1267 :param pattern: string with part of ticker, FIGI or instrument's name. 1268 :param show: if `True` then print results to console, if `False` — return list of result only. 1269 :return: list of dictionaries with all found instruments. 1270 """ 1271 if not self.iList: 1272 self.iList = self.Listing() 1273 1274 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1275 compiledPattern = re.compile(pattern, re.IGNORECASE) 1276 1277 for iType in self.iList: 1278 for instrument in self.iList[iType].values(): 1279 searchResult = compiledPattern.search(" ".join( 1280 [instrument["ticker"], instrument["figi"], instrument["name"]] 1281 )) 1282 1283 if searchResult: 1284 searchResults[iType][instrument["ticker"]] = instrument 1285 1286 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1287 info = [ 1288 "# Search results\n\n", 1289 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1290 "* **Search pattern:** [{}]\n".format(pattern), 1291 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1292 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1293 ] 1294 infoShort = info[:] 1295 1296 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1297 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1298 skippedLine = "| ... | ... | ... | ... |\n" 1299 1300 if resultsLen == 0: 1301 info.append("\nNo results\n") 1302 infoShort.append("\nNo results\n") 1303 uLogger.warning("No results. Try changing your search pattern.") 1304 1305 else: 1306 for iType in searchResults: 1307 iTypeValuesCount = len(searchResults[iType].values()) 1308 if iTypeValuesCount > 0: 1309 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 1312 for instrument in searchResults[iType].values(): 1313 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1314 instrument["type"], 1315 instrument["ticker"], 1316 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1317 instrument["figi"], 1318 )) 1319 1320 if iTypeValuesCount <= 5: 1321 infoShort.extend(info[-iTypeValuesCount:]) 1322 1323 else: 1324 infoShort.extend(info[-5:]) 1325 infoShort.append(skippedLine) 1326 1327 infoText = "".join(info) 1328 infoTextShort = "".join(infoShort) 1329 1330 if show: 1331 uLogger.info(infoTextShort) 1332 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1333 1334 if self.searchResultsFile: 1335 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1336 fH.write(infoText) 1337 1338 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1339 1340 if self.useHTMLReports: 1341 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1342 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1343 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1344 1345 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1346 1347 return searchResults 1348 1349 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1350 """ 1351 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1352 1353 :param instruments: list of strings with tickers or FIGIs. 1354 :return: list with unique instrument FIGIs only. 1355 """ 1356 requestedInstruments = [] 1357 for iName in instruments: 1358 if iName not in self.aliases.keys(): 1359 if iName not in requestedInstruments: 1360 requestedInstruments.append(iName) 1361 1362 else: 1363 if iName not in requestedInstruments: 1364 if self.aliases[iName] not in requestedInstruments: 1365 requestedInstruments.append(self.aliases[iName]) 1366 1367 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1368 1369 onlyUniqueFIGIs = [] 1370 for iName in requestedInstruments: 1371 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1372 continue 1373 1374 self._ticker = iName 1375 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1376 1377 if not iData: 1378 self._ticker = "" 1379 self._figi = iName 1380 1381 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1382 1383 if not iData: 1384 self._figi = "" 1385 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1386 1387 if iData and iData["figi"] not in onlyUniqueFIGIs: 1388 onlyUniqueFIGIs.append(iData["figi"]) 1389 1390 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1391 1392 return onlyUniqueFIGIs 1393 1394 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1395 """ 1396 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1397 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 1400 If `pricesFile` string is not empty then also save information to this file. 1401 1402 :param instruments: list of strings with tickers or FIGIs. 1403 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1404 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1405 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1406 """ 1407 if instruments is None or not instruments: 1408 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1409 raise Exception("Ticker or FIGI required") 1410 1411 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1412 1413 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1414 1415 iList = [] # trying to get info and current prices about all unique instruments: 1416 for self._figi in onlyUniqueFIGIs: 1417 iData = self.SearchByFIGI(requestPrice=True) 1418 iList.append(iData) 1419 1420 self.ShowListOfPrices(iList, show) 1421 1422 return iList 1423 1424 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1425 """ 1426 Show table contains current prices of given instruments. 1427 1428 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1429 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1430 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1431 :return: multilines text in Markdown format as a table contains current prices. 1432 """ 1433 infoText = "" 1434 1435 if show or self.pricesFile: 1436 info = [ 1437 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1438 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1439 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1440 ] 1441 1442 for item in iList: 1443 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1444 item["ticker"], 1445 item["figi"], 1446 item["type"], 1447 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1448 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1449 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1450 "{} / {}".format( 1451 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1452 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1453 ), 1454 "{} / {}".format( 1455 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1456 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1457 ), 1458 item["currency"], 1459 )) 1460 1461 infoText = "".join(info) 1462 1463 if show: 1464 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1465 1466 if self.pricesFile: 1467 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1468 fH.write(infoText) 1469 1470 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1471 1472 if self.useHTMLReports: 1473 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1474 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1475 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1476 1477 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1478 1479 return infoText 1480 1481 def RequestTradingStatus(self) -> dict: 1482 """ 1483 Requesting trading status for the instrument defined by `figi` variable. 1484 1485 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1486 1487 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1488 1489 :return: dictionary with trading status attributes. Response example: 1490 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1491 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1492 """ 1493 if self._figi is None or not self._figi: 1494 uLogger.error("Variable `figi` must be defined for using this method!") 1495 raise Exception("FIGI required") 1496 1497 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1498 1499 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1500 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1501 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1502 1503 if self.moreDebug: 1504 uLogger.debug("Records about current trading status successfully received") 1505 1506 return tradingStatus 1507 1508 def RequestPortfolio(self) -> dict: 1509 """ 1510 Requesting actual user's portfolio for current `accountId`. 1511 1512 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1513 1514 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1515 1516 :return: dictionary with user's portfolio. 1517 """ 1518 if self.accountId is None or not self.accountId: 1519 uLogger.error("Variable `accountId` must be defined for using this method!") 1520 raise Exception("Account ID required") 1521 1522 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1523 1524 self.body = str({"accountId": self.accountId}) 1525 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1526 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1527 1528 if self.moreDebug: 1529 uLogger.debug("Records about user's portfolio successfully received") 1530 1531 return rawPortfolio 1532 1533 def RequestPositions(self) -> dict: 1534 """ 1535 Requesting open positions by currencies and instruments for current `accountId`. 1536 1537 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1538 1539 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1540 1541 :return: dictionary with open positions by instruments. 1542 """ 1543 if self.accountId is None or not self.accountId: 1544 uLogger.error("Variable `accountId` must be defined for using this method!") 1545 raise Exception("Account ID required") 1546 1547 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1548 1549 self.body = str({"accountId": self.accountId}) 1550 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1551 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1552 1553 if self.moreDebug: 1554 uLogger.debug("Records about current open positions successfully received") 1555 1556 return rawPositions 1557 1558 def RequestPendingOrders(self) -> list: 1559 """ 1560 Requesting current actual pending limit orders for current `accountId`. 1561 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 1564 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1565 1566 :return: list of dictionaries with pending limit orders. 1567 """ 1568 if self.accountId is None or not self.accountId: 1569 uLogger.error("Variable `accountId` must be defined for using this method!") 1570 raise Exception("Account ID required") 1571 1572 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1573 1574 self.body = str({"accountId": self.accountId}) 1575 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1576 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1577 1578 if "orders" in rawResponse.keys(): 1579 rawOrders = rawResponse["orders"] 1580 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1581 1582 else: 1583 rawOrders = [] 1584 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1585 1586 return rawOrders 1587 1588 def RequestStopOrders(self) -> list: 1589 """ 1590 Requesting current actual stop orders for current `accountId`. 1591 1592 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1593 1594 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1595 1596 :return: list of dictionaries with stop orders. 1597 """ 1598 if self.accountId is None or not self.accountId: 1599 uLogger.error("Variable `accountId` must be defined for using this method!") 1600 raise Exception("Account ID required") 1601 1602 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1603 1604 self.body = str({"accountId": self.accountId}) 1605 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1606 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1607 1608 if "stopOrders" in rawResponse.keys(): 1609 rawStopOrders = rawResponse["stopOrders"] 1610 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1611 1612 else: 1613 rawStopOrders = [] 1614 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1615 1616 return rawStopOrders 1617 1618 def Overview(self, show: bool = False, details: str = "full") -> dict: 1619 """ 1620 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1621 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1622 and `overviewBondsCalendarFile` are defined then also save information to file. 1623 1624 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1625 many requests about the state of the portfolio, and then, based on the received data, a large number 1626 of calculation and statistics are collected. 1627 1628 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1629 :param details: how detailed should the information be? 1630 - `full` — shows full available information about portfolio status (by default), 1631 - `positions` — shows only open positions, 1632 - `orders` — shows only sections of open limits and stop orders. 1633 - `digest` — show a short digest of the portfolio status, 1634 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1635 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1636 :return: dictionary with client's raw portfolio and some statistics. 1637 """ 1638 if self.accountId is None or not self.accountId: 1639 uLogger.error("Variable `accountId` must be defined for using this method!") 1640 raise Exception("Account ID required") 1641 1642 view = { 1643 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1644 "headers": {}, # list of dictionaries, response headers without "positions" section 1645 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1646 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1647 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1648 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1649 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1650 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1651 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1652 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1653 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1654 }, 1655 "stat": { # --- some statistics calculated using "raw" sections: 1656 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1657 "availableRUB": 0., # available rubles (without other currencies) 1658 "blockedRUB": 0., # blocked sum in Russian Rouble 1659 "totalChangesRUB": 0., # changes for all open trades in RUB 1660 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1661 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1662 "sharesCostRUB": 0., # costs of all shares in RUB 1663 "bondsCostRUB": 0., # costs of all bonds in RUB 1664 "etfsCostRUB": 0., # costs of all etfs in RUB 1665 "futuresCostRUB": 0., # costs of all futures in RUB 1666 "Currencies": [], # list of dictionaries of all currencies statistics 1667 "Shares": [], # list of dictionaries of all shares statistics 1668 "Bonds": [], # list of dictionaries of all bonds statistics 1669 "Etfs": [], # list of dictionaries of all etfs statistics 1670 "Futures": [], # list of dictionaries of all futures statistics 1671 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1672 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1673 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1674 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1675 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1676 }, 1677 "analytics": { # --- some analytics of portfolio: 1678 "distrByAssets": {}, # portfolio distribution by assets 1679 "distrByCompanies": {}, # portfolio distribution by companies 1680 "distrBySectors": {}, # portfolio distribution by sectors 1681 "distrByCurrencies": {}, # portfolio distribution by currencies 1682 "distrByCountries": {}, # portfolio distribution by countries 1683 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1684 } 1685 } 1686 1687 details = details.lower() 1688 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1689 if details not in availableDetails: 1690 details = "full" 1691 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1692 1693 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1694 1695 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1696 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1697 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1698 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1699 1700 # save response headers without "positions" section: 1701 for key in portfolioResponse.keys(): 1702 if key != "positions": 1703 view["raw"]["headers"][key] = portfolioResponse[key] 1704 1705 else: 1706 continue 1707 1708 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1709 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1710 for item in portfolioResponse["positions"]: 1711 if item["instrumentType"] == "currency": 1712 self._figi = item["figi"] 1713 if not self._figi and item["ticker"]: 1714 self._ticker = item["ticker"] 1715 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1716 1717 curr = self.SearchByFIGI(requestPrice=False) 1718 1719 # current price of currency in RUB: 1720 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1721 "name": curr["name"], 1722 "currentPrice": NanoToFloat( 1723 item["currentPrice"]["units"], 1724 item["currentPrice"]["nano"] 1725 ), 1726 } 1727 1728 view["raw"]["Currencies"].append(item) 1729 1730 elif item["instrumentType"] == "share": 1731 view["raw"]["Shares"].append(item) 1732 1733 elif item["instrumentType"] == "bond": 1734 view["raw"]["Bonds"].append(item) 1735 1736 elif item["instrumentType"] == "etf": 1737 view["raw"]["Etfs"].append(item) 1738 1739 elif item["instrumentType"] == "futures": 1740 view["raw"]["Futures"].append(item) 1741 1742 else: 1743 continue 1744 1745 # how many volume of currencies (by ISO currency name) are blocked: 1746 for item in view["raw"]["positions"]["blocked"]: 1747 blocked = NanoToFloat(item["units"], item["nano"]) 1748 if blocked > 0: 1749 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1750 1751 # how many volume of instruments (by FIGI) are blocked: 1752 for item in view["raw"]["positions"]["securities"]: 1753 blocked = int(item["blocked"]) 1754 if blocked > 0: 1755 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1756 1757 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1758 1759 if "rub" in allBlocked.keys(): 1760 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1761 1762 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1763 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1764 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1765 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1766 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1767 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1768 view["stat"]["portfolioCostRUB"] = sum([ 1769 view["stat"]["allCurrenciesCostRUB"], 1770 view["stat"]["sharesCostRUB"], 1771 view["stat"]["bondsCostRUB"], 1772 view["stat"]["etfsCostRUB"], 1773 view["stat"]["futuresCostRUB"], 1774 ]) 1775 1776 # --- calculating some portfolio statistics: 1777 byComp = {} # distribution by companies 1778 bySect = {} # distribution by sectors 1779 byCurr = {} # distribution by currencies (include RUB) 1780 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1781 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1782 1783 for item in portfolioResponse["positions"]: 1784 self._figi = item["figi"] 1785 if not self._figi and item["ticker"]: 1786 self._ticker = item["ticker"] 1787 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1788 1789 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1790 1791 if instrument: 1792 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1793 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1794 1795 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1796 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1797 1798 else: 1799 blocked = 0 1800 1801 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1802 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1803 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1804 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1805 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1806 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1807 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1808 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1809 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1810 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1811 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1812 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1813 1814 statData = { 1815 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1816 "ticker": instrument["ticker"], # ticker by FIGI 1817 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1818 "volume": volume, # available volume of instrument 1819 "lots": lots, # volume in lots of instrument 1820 "direction": direction, # direction of an instrument's position: short or long 1821 "blocked": blocked, # blocked volume of currency or instrument 1822 "currentPrice": curPrice, # current instrument's price in basic asset 1823 "average": average, # current average position price 1824 "cost": cost, # current cost of all volume of instrument in basic asset 1825 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1826 "costRUB": costRUB, # cost of instrument in ruble 1827 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1828 "profit": profit, # expected profit at current moment 1829 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1830 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1831 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1832 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1833 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1834 "step": instrument["step"], # minimum price increment 1835 } 1836 1837 # adding distribution by unique countries: 1838 if statData["country"] not in byCountry.keys(): 1839 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1840 1841 else: 1842 byCountry[statData["country"]]["cost"] += costRUB 1843 byCountry[statData["country"]]["percent"] += percentCostRUB 1844 1845 if item["instrumentType"] != "currency": 1846 # adding distribution by unique companies: 1847 if statData["name"]: 1848 if statData["name"] not in byComp.keys(): 1849 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1850 1851 else: 1852 byComp[statData["name"]]["cost"] += costRUB 1853 byComp[statData["name"]]["percent"] += percentCostRUB 1854 1855 # adding distribution by unique sectors: 1856 if statData["sector"] not in bySect.keys(): 1857 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1858 1859 else: 1860 bySect[statData["sector"]]["cost"] += costRUB 1861 bySect[statData["sector"]]["percent"] += percentCostRUB 1862 1863 # adding distribution by unique currencies: 1864 if currency not in byCurr.keys(): 1865 byCurr[currency] = { 1866 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1867 "cost": costRUB, 1868 "percent": percentCostRUB 1869 } 1870 1871 else: 1872 byCurr[currency]["cost"] += costRUB 1873 byCurr[currency]["percent"] += percentCostRUB 1874 1875 # saving statistics for every instrument: 1876 if item["instrumentType"] == "currency": 1877 view["stat"]["Currencies"].append(statData) 1878 1879 # update dict with free funds for trading (total - blocked) by currencies 1880 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1881 view["stat"]["funds"][currency] = { 1882 "total": volume, 1883 "totalCostRUB": costRUB, # total volume cost in rubles 1884 "free": volume - blocked, 1885 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1886 } 1887 1888 elif item["instrumentType"] == "share": 1889 view["stat"]["Shares"].append(statData) 1890 1891 elif item["instrumentType"] == "bond": 1892 view["stat"]["Bonds"].append(statData) 1893 1894 elif item["instrumentType"] == "etf": 1895 view["stat"]["Etfs"].append(statData) 1896 1897 elif item["instrumentType"] == "Futures": 1898 view["stat"]["Futures"].append(statData) 1899 1900 else: 1901 continue 1902 1903 # total changes in Russian Ruble: 1904 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1905 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1906 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1907 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1908 view["stat"]["funds"]["rub"] = { 1909 "total": view["stat"]["availableRUB"], 1910 "totalCostRUB": view["stat"]["availableRUB"], 1911 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1912 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1913 } 1914 1915 # --- pending limit orders sector data: 1916 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1917 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1918 1919 for item in view["raw"]["orders"]: 1920 self._figi = item["figi"] 1921 1922 if item["figi"] not in uniquePendingOrdersFIGIs: 1923 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1924 1925 uniquePendingOrdersFIGIs.append(item["figi"]) 1926 uniquePendingOrders[item["figi"]] = instrument 1927 1928 else: 1929 instrument = uniquePendingOrders[item["figi"]] 1930 1931 if instrument: 1932 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1933 orderType = TKS_ORDER_TYPES[item["orderType"]] 1934 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1935 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1936 1937 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1938 if item["direction"] == "ORDER_DIRECTION_BUY": 1939 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1940 1941 else: 1942 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1943 1944 # requested price for order execution: 1945 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1946 1947 # necessary changes in percent to reach target from current price: 1948 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1949 1950 view["stat"]["orders"].append({ 1951 "orderID": item["orderId"], # orderId number parameter of current order 1952 "figi": item["figi"], # FIGI identification 1953 "ticker": instrument["ticker"], # ticker name by FIGI 1954 "lotsRequested": item["lotsRequested"], # requested lots value 1955 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1956 "currentPrice": lastPrice, # current instrument's price for defined action 1957 "targetPrice": target, # requested price for order execution in base currency 1958 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1959 "percentChanges": changes, # changes in percent to target from current price 1960 "currency": item["currency"], # instrument's currency name 1961 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1962 "type": orderType, # type of order from TKS_ORDER_TYPES 1963 "status": orderState, # order status from TKS_ORDER_STATES 1964 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1965 }) 1966 1967 # --- stop orders sector data: 1968 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1969 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1970 1971 for item in view["raw"]["stopOrders"]: 1972 self._figi = item["figi"] 1973 1974 if item["figi"] not in uniqueStopOrdersFIGIs: 1975 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1976 1977 uniqueStopOrdersFIGIs.append(item["figi"]) 1978 uniqueStopOrders[item["figi"]] = instrument 1979 1980 else: 1981 instrument = uniqueStopOrders[item["figi"]] 1982 1983 if instrument: 1984 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1985 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1986 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1987 1988 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1989 if "expirationTime" in item.keys(): 1990 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1991 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1992 1993 else: 1994 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1995 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1996 1997 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1998 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1999 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2000 2001 else: 2002 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2003 2004 # requested price when stop-order executed: 2005 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2006 2007 # price for limit-order, set up when stop-order executed: 2008 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2009 2010 # necessary changes in percent to reach target from current price: 2011 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2012 2013 view["stat"]["stopOrders"].append({ 2014 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2015 "figi": item["figi"], # FIGI identification 2016 "ticker": instrument["ticker"], # ticker name by FIGI 2017 "lotsRequested": item["lotsRequested"], # requested lots value 2018 "currentPrice": lastPrice, # current instrument's price for defined action 2019 "targetPrice": target, # requested price for stop-order execution in base currency 2020 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2021 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2022 "percentChanges": changes, # changes in percent to target from current price 2023 "currency": item["currency"], # instrument's currency name 2024 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2025 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2026 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2027 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2028 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2029 }) 2030 2031 # --- calculating data for analytics section: 2032 # portfolio distribution by assets: 2033 view["analytics"]["distrByAssets"] = { 2034 "Ruble": { 2035 "uniques": 1, 2036 "cost": view["stat"]["availableRUB"], 2037 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Currencies": { 2040 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2041 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2042 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Shares": { 2045 "uniques": len(view["stat"]["Shares"]), 2046 "cost": view["stat"]["sharesCostRUB"], 2047 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Bonds": { 2050 "uniques": len(view["stat"]["Bonds"]), 2051 "cost": view["stat"]["bondsCostRUB"], 2052 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Etfs": { 2055 "uniques": len(view["stat"]["Etfs"]), 2056 "cost": view["stat"]["etfsCostRUB"], 2057 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 "Futures": { 2060 "uniques": len(view["stat"]["Futures"]), 2061 "cost": view["stat"]["futuresCostRUB"], 2062 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2063 }, 2064 } 2065 2066 # portfolio distribution by companies: 2067 view["analytics"]["distrByCompanies"]["All money cash"] = { 2068 "ticker": "", 2069 "cost": view["stat"]["allCurrenciesCostRUB"], 2070 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2071 } 2072 view["analytics"]["distrByCompanies"].update(byComp) 2073 2074 # portfolio distribution by sectors: 2075 view["analytics"]["distrBySectors"]["All money cash"] = { 2076 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2077 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2078 } 2079 view["analytics"]["distrBySectors"].update(bySect) 2080 2081 # portfolio distribution by currencies: 2082 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2083 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2084 2085 if self.moreDebug: 2086 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2087 2088 view["analytics"]["distrByCurrencies"].update(byCurr) 2089 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2090 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2091 2092 # portfolio distribution by countries: 2093 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2094 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2095 2096 if self.moreDebug: 2097 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2098 2099 view["analytics"]["distrByCountries"].update(byCountry) 2100 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2101 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2102 2103 # --- Prepare text statistics overview in human-readable: 2104 if show: 2105 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2106 2107 # Whatever the value `details`, header not changes: 2108 info = [ 2109 "# Client's portfolio\n\n", 2110 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2111 "* **Account ID:** [{}]\n".format(self.accountId), 2112 ] 2113 2114 if details in ["full", "positions", "digest"]: 2115 info.extend([ 2116 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2117 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2118 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2119 view["stat"]["totalChangesRUB"], 2120 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2121 view["stat"]["totalChangesPercentRUB"], 2122 ), 2123 ]) 2124 2125 if details in ["full", "positions"]: 2126 info.extend([ 2127 "## Open positions\n\n", 2128 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2129 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2130 "| **Ruble:** | {:>31} | | | | | |\n".format( 2131 "{:.2f} ({:.2f}) rub".format( 2132 view["stat"]["availableRUB"], 2133 view["stat"]["blockedRUB"], 2134 ) 2135 ) 2136 ]) 2137 2138 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2139 return [ 2140 "| | | | | | | |\n", 2141 "| {:<27} | | | | | {:>19} | |\n".format( 2142 noTradeStr if noTradeStr else typeStr, 2143 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2144 ), 2145 ] 2146 2147 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2148 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2149 "{} [{}]".format(data["ticker"], data["figi"]), 2150 "{:.2f} ({:.2f}) {}".format( 2151 data["volume"], 2152 data["blocked"], 2153 data["currency"], 2154 ) if isCurr else "{:.0f} ({:.0f})".format( 2155 data["volume"], 2156 data["blocked"], 2157 ), 2158 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2159 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2160 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2161 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2162 "{}{:.2f} {} ({}{:.2f}%)".format( 2163 "+" if data["profit"] > 0 else "", 2164 data["profit"], data["baseCurrencyName"], 2165 "+" if data["percentProfit"] > 0 else "", 2166 data["percentProfit"], 2167 ), 2168 ) 2169 2170 # --- Show currencies section: 2171 if view["stat"]["Currencies"]: 2172 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2173 for item in view["stat"]["Currencies"]: 2174 info.append(_InfoStr(item, isCurr=True)) 2175 2176 else: 2177 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2178 2179 # --- Show shares section: 2180 if view["stat"]["Shares"]: 2181 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2182 2183 for item in view["stat"]["Shares"]: 2184 info.append(_InfoStr(item)) 2185 2186 else: 2187 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2188 2189 # --- Show bonds section: 2190 if view["stat"]["Bonds"]: 2191 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2192 2193 for item in view["stat"]["Bonds"]: 2194 info.append(_InfoStr(item)) 2195 2196 else: 2197 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2198 2199 # --- Show etfs section: 2200 if view["stat"]["Etfs"]: 2201 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2202 2203 for item in view["stat"]["Etfs"]: 2204 info.append(_InfoStr(item)) 2205 2206 else: 2207 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2208 2209 # --- Show futures section: 2210 if view["stat"]["Futures"]: 2211 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2212 2213 for item in view["stat"]["Futures"]: 2214 info.append(_InfoStr(item)) 2215 2216 else: 2217 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2218 2219 if details in ["full", "orders"]: 2220 # --- Show pending limit orders section: 2221 if view["stat"]["orders"]: 2222 info.extend([ 2223 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2224 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2225 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2226 ]) 2227 2228 for item in view["stat"]["orders"]: 2229 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2230 "{} [{}]".format(item["ticker"], item["figi"]), 2231 item["orderID"], 2232 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2233 "{} {} ({}{:.2f}%)".format( 2234 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2235 item["baseCurrencyName"], 2236 "+" if item["percentChanges"] > 0 else "", 2237 float(item["percentChanges"]), 2238 ), 2239 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2240 item["action"], 2241 item["type"], 2242 item["date"], 2243 )) 2244 2245 else: 2246 info.append("\n## Total pending limit-orders: [0]\n") 2247 2248 # --- Show stop orders section: 2249 if view["stat"]["stopOrders"]: 2250 info.extend([ 2251 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2252 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2253 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2254 ]) 2255 2256 for item in view["stat"]["stopOrders"]: 2257 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2258 "{} [{}]".format(item["ticker"], item["figi"]), 2259 item["orderID"], 2260 item["lotsRequested"], 2261 "{} {} ({}{:.2f}%)".format( 2262 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2263 item["baseCurrencyName"], 2264 "+" if item["percentChanges"] > 0 else "", 2265 float(item["percentChanges"]), 2266 ), 2267 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2268 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2269 item["action"], 2270 item["type"], 2271 item["expType"], 2272 item["createDate"], 2273 item["expDate"], 2274 )) 2275 2276 else: 2277 info.append("\n## Total stop-orders: [0]\n") 2278 2279 if details in ["full", "analytics"]: 2280 # -- Show analytics section: 2281 if view["stat"]["portfolioCostRUB"] > 0: 2282 info.extend([ 2283 "\n# Analytics\n\n" 2284 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2285 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2286 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2287 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2288 view["stat"]["totalChangesRUB"], 2289 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2290 view["stat"]["totalChangesPercentRUB"], 2291 ), 2292 "\n## Portfolio distribution by assets\n" 2293 "\n| Type | Uniques | Percent | Current cost |\n", 2294 "|------------------------------------|---------|---------|--------------------|\n", 2295 ]) 2296 2297 for key in view["analytics"]["distrByAssets"].keys(): 2298 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2299 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2300 key, 2301 view["analytics"]["distrByAssets"][key]["uniques"], 2302 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2304 )) 2305 2306 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2307 2308 info.extend([ 2309 "\n## Portfolio distribution by companies\n" 2310 "\n| Company | Percent | Current cost |\n", 2311 aSepLine, 2312 ]) 2313 2314 for company in view["analytics"]["distrByCompanies"].keys(): 2315 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2316 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2317 "{}{}".format( 2318 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2319 company, 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2323 )) 2324 2325 info.extend([ 2326 "\n## Portfolio distribution by sectors\n" 2327 "\n| Sector | Percent | Current cost |\n", 2328 aSepLine, 2329 ]) 2330 2331 for sector in view["analytics"]["distrBySectors"].keys(): 2332 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2333 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2334 sector, 2335 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2337 )) 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by currencies\n" 2341 "\n| Instruments currencies | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for curr in view["analytics"]["distrByCurrencies"].keys(): 2346 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2349 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2351 )) 2352 2353 info.extend([ 2354 "\n## Portfolio distribution by countries\n" 2355 "\n| Assets by country | Percent | Current cost |\n", 2356 aSepLine, 2357 ]) 2358 2359 for country in view["analytics"]["distrByCountries"].keys(): 2360 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2361 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2362 country, 2363 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2364 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2365 )) 2366 2367 if details in ["full", "calendar"]: 2368 # -- Show bonds payment calendar section: 2369 if view["stat"]["Bonds"]: 2370 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2371 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2372 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2373 2374 else: 2375 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2376 2377 infoText = "".join(info) 2378 2379 uLogger.info(infoText) 2380 2381 if details == "full" and self.overviewFile: 2382 filename = self.overviewFile 2383 2384 elif details == "digest" and self.overviewDigestFile: 2385 filename = self.overviewDigestFile 2386 2387 elif details == "positions" and self.overviewPositionsFile: 2388 filename = self.overviewPositionsFile 2389 2390 elif details == "orders" and self.overviewOrdersFile: 2391 filename = self.overviewOrdersFile 2392 2393 elif details == "analytics" and self.overviewAnalyticsFile: 2394 filename = self.overviewAnalyticsFile 2395 2396 elif details == "calendar" and self.overviewBondsCalendarFile: 2397 filename = self.overviewBondsCalendarFile 2398 2399 else: 2400 filename = "" 2401 2402 if filename: 2403 with open(filename, "w", encoding="UTF-8") as fH: 2404 fH.write(infoText) 2405 2406 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2407 2408 if self.useHTMLReports: 2409 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2410 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2411 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2412 2413 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2414 2415 return view 2416 2417 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2418 """ 2419 Returns history operations between two given dates for current `accountId`. 2420 If `reportFile` string is not empty then also save human-readable report. 2421 Shows some statistical data of closed positions. 2422 2423 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2425 :param show: if `True` then also prints all records to the console. 2426 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2427 :return: original list of dictionaries with history of deals records from API ("operations" key): 2428 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2429 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2430 """ 2431 if self.accountId is None or not self.accountId: 2432 uLogger.error("Variable `accountId` must be defined for using this method!") 2433 raise Exception("Account ID required") 2434 2435 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2436 2437 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2438 2439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2440 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2441 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2442 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2443 customStat = {} # custom statistics in additional to responseJSON 2444 2445 # --- output report in human-readable format: 2446 if show or self.reportFile: 2447 splitLine1 = "| | | | | |\n" # Summary section 2448 splitLine2 = "| | | | | | | | |\n" # Operations section 2449 nextDay = "" 2450 2451 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2452 2453 if len(ops) > 0: 2454 customStat = { 2455 "opsCount": 0, # total operations count 2456 "buyCount": 0, # buy operations 2457 "sellCount": 0, # sell operations 2458 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2459 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2460 "payIn": {"rub": 0.}, # Deposit brokerage account 2461 "payOut": {"rub": 0.}, # Withdrawals 2462 "divs": {"rub": 0.}, # Dividends income 2463 "coupons": {"rub": 0.}, # Coupon's income 2464 "brokerCom": {"rub": 0.}, # Service commissions 2465 "serviceCom": {"rub": 0.}, # Service commissions 2466 "marginCom": {"rub": 0.}, # Margin commissions 2467 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2468 } 2469 2470 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2471 for item in ops: 2472 if item["state"] == "OPERATION_STATE_EXECUTED": 2473 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2474 2475 # count buy operations: 2476 if "_BUY" in item["operationType"]: 2477 customStat["buyCount"] += 1 2478 2479 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2480 customStat["buyTotal"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["buyTotal"][item["payment"]["currency"]] = payment 2484 2485 # count sell operations: 2486 elif "_SELL" in item["operationType"]: 2487 customStat["sellCount"] += 1 2488 2489 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2490 customStat["sellTotal"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["sellTotal"][item["payment"]["currency"]] = payment 2494 2495 # count incoming operations: 2496 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2497 if item["payment"]["currency"] in customStat["payIn"].keys(): 2498 customStat["payIn"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["payIn"][item["payment"]["currency"]] = payment 2502 2503 # count withdrawals operations: 2504 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2505 if item["payment"]["currency"] in customStat["payOut"].keys(): 2506 customStat["payOut"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["payOut"][item["payment"]["currency"]] = payment 2510 2511 # count dividends income: 2512 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2513 if item["payment"]["currency"] in customStat["divs"].keys(): 2514 customStat["divs"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["divs"][item["payment"]["currency"]] = payment 2518 2519 # count coupon's income: 2520 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2521 if item["payment"]["currency"] in customStat["coupons"].keys(): 2522 customStat["coupons"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["coupons"][item["payment"]["currency"]] = payment 2526 2527 # count broker commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2529 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2530 customStat["brokerCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["brokerCom"][item["payment"]["currency"]] = payment 2534 2535 # count service commissions: 2536 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2537 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2538 customStat["serviceCom"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["serviceCom"][item["payment"]["currency"]] = payment 2542 2543 # count margin commissions: 2544 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2545 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2546 customStat["marginCom"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["marginCom"][item["payment"]["currency"]] = payment 2550 2551 # count withholding taxes: 2552 elif "_TAX" in item["operationType"]: 2553 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2554 customStat["allTaxes"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["allTaxes"][item["payment"]["currency"]] = payment 2558 2559 else: 2560 continue 2561 2562 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2563 2564 # --- view "Actions" lines: 2565 info.extend([ 2566 "| Report sections | | | | |\n", 2567 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2568 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2569 "| | Buy: {:<22} | {:<28} | | |\n".format( 2570 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2571 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2572 ), 2573 "| | Sell: {:<21} | {:<28} | | |\n".format( 2574 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2575 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2576 ), 2577 ]) 2578 2579 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2580 for key in opsKeys: 2581 if key == "rub": 2582 continue 2583 2584 info.extend([ 2585 "| | | {:<28} | | |\n".format( 2586 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2587 ), 2588 "| | | {:<28} | | |\n".format( 2589 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2590 ), 2591 ]) 2592 2593 info.append(splitLine1) 2594 2595 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2596 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2597 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2601 ) 2602 2603 # --- view "Payments" lines: 2604 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2605 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2606 2607 for key in paymentsKeys: 2608 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2609 2610 info.append(splitLine1) 2611 2612 # --- view "Commissions and taxes" lines: 2613 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2614 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2615 2616 for key in comKeys: 2617 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2618 2619 info.extend([ 2620 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2621 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2622 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2623 ]) 2624 2625 else: 2626 info.append("Broker returned no operations during this period\n") 2627 2628 # --- view "Operations" section: 2629 for item in ops: 2630 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2631 continue 2632 2633 else: 2634 self._figi = item["figi"] 2635 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2636 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2637 2638 # group of deals during one day: 2639 if nextDay and item["date"].split("T")[0] != nextDay: 2640 info.append(splitLine2) 2641 nextDay = "" 2642 2643 else: 2644 nextDay = item["date"].split("T")[0] # saving current day for splitting 2645 2646 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2647 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2648 self._figi if self._figi else "—", 2649 instrument["ticker"] if instrument else "—", 2650 instrument["type"] if instrument else "—", 2651 item["quantity"] if int(item["quantity"]) > 0 else "—", 2652 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2653 TKS_OPERATION_STATES[item["state"]], 2654 TKS_OPERATION_TYPES[item["operationType"]], 2655 )) 2656 2657 infoText = "".join(info) 2658 2659 if show: 2660 if self.moreDebug: 2661 uLogger.debug("Records about history of a client's operations successfully received") 2662 2663 uLogger.info(infoText) 2664 2665 if self.reportFile: 2666 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2667 fH.write(infoText) 2668 2669 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2670 2671 if self.useHTMLReports: 2672 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2673 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2674 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2675 2676 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2677 2678 return ops, customStat 2679 2680 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2681 """ 2682 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2683 2684 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2685 Warning! Broker server used ISO UTC time by default. 2686 2687 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2688 Also, `historyFile` used to update history with `onlyMissing` parameter. 2689 2690 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2691 2692 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2695 `"hour"`, `"day"`. Default: `"hour"`. 2696 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2697 False by default. Warning! History appends only from last candle to current time 2698 with always update last candle! 2699 :param csvSep: separator if csv-file is used, `,` by default. 2700 :param show: if `True` then also prints Pandas DataFrame to the console. 2701 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2702 `["date", "time", "open", "high", "low", "close", "volume"]`. 2703 """ 2704 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2705 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2706 history = None # empty pandas object for history 2707 2708 if interval not in TKS_CANDLE_INTERVALS.keys(): 2709 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2710 raise Exception("Incorrect value") 2711 2712 if not (self._ticker or self._figi): 2713 uLogger.error("Ticker or FIGI must be defined!") 2714 raise Exception("Ticker or FIGI required") 2715 2716 if self._ticker and not self._figi: 2717 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2718 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2719 2720 if self._figi and not self._ticker: 2721 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2722 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2723 2724 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2725 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2726 if interval.lower() != "day": 2727 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2728 2729 delta = dtEnd - dtStart # current UTC time minus last time in file 2730 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2731 2732 # calculate history length in candles: 2733 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2734 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2735 length += 1 # to avoid fraction time 2736 2737 # calculate data blocks count: 2738 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2739 2740 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2741 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2742 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2743 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2744 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2745 2746 tempOld = None # pandas object for old history, if --only-missing key present 2747 lastTime = None # datetime object of last old candle in file 2748 2749 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2750 uLogger.debug("--only-missing key present, add only last missing candles...") 2751 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2752 2753 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2754 2755 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2756 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2757 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2758 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2759 2760 # get last datetime object from last string in file or minus 1 delta if file is empty: 2761 if len(tempOld) > 0: 2762 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2763 2764 else: 2765 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2766 2767 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2768 2769 responseJSONs = [] # raw history blocks of data 2770 2771 blockEnd = dtEnd 2772 for item in range(blocks): 2773 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2774 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2775 2776 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2777 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2778 )) 2779 2780 if blockStart == blockEnd: 2781 uLogger.debug("Skipped this zero-length block...") 2782 2783 else: 2784 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2785 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2786 self.body = str({ 2787 "figi": self._figi, 2788 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2789 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2790 "interval": TKS_CANDLE_INTERVALS[interval][0] 2791 }) 2792 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2793 2794 if "code" in responseJSON.keys(): 2795 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2796 2797 else: 2798 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2799 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2800 2801 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2802 2803 blockEnd = blockStart 2804 2805 printCount = len(responseJSONs) # candles to show in console 2806 if responseJSONs: 2807 tempHistory = pd.DataFrame( 2808 data={ 2809 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2810 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2811 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2812 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2813 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2814 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2815 "volume": [int(item["volume"]) for item in responseJSONs], 2816 }, 2817 index=range(len(responseJSONs)), 2818 columns=["date", "time", "open", "high", "low", "close", "volume"], 2819 ) 2820 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2821 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2822 2823 # append only newest candles to old history if --only-missing key present: 2824 if onlyMissing and tempOld is not None and lastTime is not None: 2825 index = 0 # find start index in tempHistory data: 2826 2827 for i, item in tempHistory.iterrows(): 2828 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2829 2830 if curTime == lastTime: 2831 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2832 index = i 2833 printCount = index + 1 2834 break 2835 2836 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2837 2838 else: 2839 history = tempHistory # if no `--only-missing` key then load full data from server 2840 2841 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2842 2843 if history is not None and not history.empty: 2844 if show: 2845 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2846 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2847 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2848 )) 2849 2850 else: 2851 uLogger.warning("Received an empty candles history!") 2852 2853 if self.historyFile is not None: 2854 if history is not None and not history.empty: 2855 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2856 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2857 2858 else: 2859 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2860 2861 else: 2862 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2863 2864 return history 2865 2866 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2867 """ 2868 Load candles history from csv-file and return Pandas DataFrame object. 2869 2870 See also: `History()` and `ShowHistoryChart()` methods. 2871 2872 :param filePath: path to csv-file to open. 2873 """ 2874 loadedHistory = None # init candles data object 2875 2876 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2877 2878 if os.path.exists(filePath): 2879 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2880 2881 tfStr = self.priceModel.FormattedDelta( 2882 self.priceModel.timeframe, 2883 "{days} days {hours}h {minutes}m {seconds}s", 2884 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2885 self.priceModel.timeframe, 2886 "{hours}h {minutes}m {seconds}s", 2887 ) 2888 2889 if loadedHistory is not None and not loadedHistory.empty: 2890 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2891 len(loadedHistory), 2892 tfStr, 2893 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2894 ) 2895 2896 else: 2897 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2898 2899 else: 2900 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2901 2902 return loadedHistory 2903 2904 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2905 """ 2906 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2907 2908 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2909 Default: `index.html` (both for interact and non-interact candlesticks chart). 2910 2911 See also: `History()` and `LoadHistory()` methods. 2912 2913 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2914 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2915 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2916 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2917 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2918 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2919 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2920 """ 2921 if isinstance(candles, str): 2922 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2923 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2924 2925 elif isinstance(candles, pd.DataFrame): 2926 self.priceModel.prices = candles # set candles chain from variable 2927 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2928 2929 if "datetime" not in candles.columns: 2930 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2931 2932 else: 2933 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2934 raise Exception("Incorrect value") 2935 2936 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2937 2938 if interact: 2939 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2940 2941 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2942 2943 else: 2944 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2945 2946 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2947 2948 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2949 2950 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2951 """ 2952 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2953 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2954 2955 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2956 2957 :param operation: string "Buy" or "Sell". 2958 :param lots: volume, integer count of lots >= 1. 2959 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2960 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2961 :param expDate: string "Undefined" by default or local date in future, 2962 it is a string with format `%Y-%m-%d %H:%M:%S`. 2963 :return: JSON with response from broker server. 2964 """ 2965 if self.accountId is None or not self.accountId: 2966 uLogger.error("Variable `accountId` must be defined for using this method!") 2967 raise Exception("Account ID required") 2968 2969 if operation is None or not operation or operation not in ("Buy", "Sell"): 2970 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2971 raise Exception("Incorrect value") 2972 2973 if lots is None or lots < 1: 2974 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2975 lots = 1 2976 2977 if tp is None or tp < 0: 2978 tp = 0 2979 2980 if sl is None or sl < 0: 2981 sl = 0 2982 2983 if expDate is None or not expDate: 2984 expDate = "Undefined" 2985 2986 if not (self._ticker or self._figi): 2987 uLogger.error("Ticker or FIGI must be defined!") 2988 raise Exception("Ticker or FIGI required") 2989 2990 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2991 self._ticker = instrument["ticker"] 2992 self._figi = instrument["figi"] 2993 2994 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2995 2996 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2997 self.body = str({ 2998 "figi": self._figi, 2999 "quantity": str(lots), 3000 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3001 "accountId": str(self.accountId), 3002 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3003 }) 3004 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3005 3006 if "orderId" in response.keys(): 3007 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3008 operation, response["orderId"], 3009 self._ticker, self._figi, lots, 3010 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3011 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3012 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3013 )) 3014 3015 if tp > 0: 3016 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3017 3018 if sl > 0: 3019 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3020 3021 else: 3022 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3023 3024 return response 3025 3026 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3027 """ 3028 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3029 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3030 3031 See also: `Order()` and `Trade()` docstrings. 3032 3033 :param lots: volume, integer count of lots >= 1. 3034 :param tp: float > 0, take profit price of stop-order. 3035 :param sl: float > 0, stop loss price of stop-order. 3036 :param expDate: it's a local date in future. 3037 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3038 :return: JSON with response from broker server. 3039 """ 3040 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3041 3042 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3043 """ 3044 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3045 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3046 3047 See also: `Order()` and `Trade()` docstrings. 3048 3049 :param lots: volume, integer count of lots >= 1. 3050 :param tp: float > 0, take profit price of stop-order. 3051 :param sl: float > 0, stop loss price of stop-order. 3052 :param expDate: it's a local date in the future. 3053 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3054 :return: JSON with response from broker server. 3055 """ 3056 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3057 3058 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3059 """ 3060 Close position of given instruments. 3061 3062 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3063 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3064 This avoids unnecessary downloading data from the server. 3065 """ 3066 if instruments is None or not instruments: 3067 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3068 raise Exception("Ticker or FIGI required") 3069 3070 if isinstance(instruments, str): 3071 instruments = [instruments] 3072 3073 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3074 if uniqueInstruments: 3075 if portfolio is None or not portfolio: 3076 portfolio = self.Overview(show=False) 3077 3078 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3079 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3080 3081 for self._figi in uniqueInstruments: 3082 if self._figi not in allOpened: 3083 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3084 continue 3085 3086 # search open trade info about instrument by ticker: 3087 instrument = {} 3088 for iType in TKS_INSTRUMENTS: 3089 if instrument: 3090 break 3091 3092 for item in portfolio["stat"][iType]: 3093 if item["figi"] == self._figi: 3094 instrument = item 3095 break 3096 3097 if instrument: 3098 self._ticker = instrument["ticker"] 3099 self._figi = instrument["figi"] 3100 3101 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3102 self._ticker, 3103 self._figi, 3104 int(instrument["volume"]), 3105 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3106 )) 3107 3108 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3109 3110 if tradeLots > 0: 3111 if instrument["blocked"] > 0: 3112 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3113 instrument["blocked"], 3114 self._ticker, 3115 tradeLots, 3116 )) 3117 3118 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3119 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3120 3121 else: 3122 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3123 3124 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3125 """ 3126 Close all positions of given instruments with defined type. 3127 3128 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3129 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3130 This avoids unnecessary downloading data from the server. 3131 """ 3132 if iType not in TKS_INSTRUMENTS: 3133 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3134 3135 else: 3136 if portfolio is None or not portfolio: 3137 portfolio = self.Overview(show=False) 3138 3139 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3140 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3141 3142 if tickers and portfolio: 3143 self.CloseTrades(tickers, portfolio) 3144 3145 else: 3146 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3147 3148 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3149 """ 3150 Universal method to create market or limit orders with all available parameters for current `accountId`. 3151 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3152 3153 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3154 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3155 3156 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3157 then broker immediately open market order as you can do simple --buy or --sell operations! 3158 3159 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3160 When current price will go up or down to target price value then broker opens a limit order. 3161 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3162 3163 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3164 3165 :param operation: string "Buy" or "Sell". 3166 :param orderType: string "Limit" or "Stop". 3167 :param lots: volume, integer count of lots >= 1. 3168 :param targetPrice: target price > 0. This is open trade price for limit order. 3169 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3170 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3171 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3172 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3173 Stop loss order always executed by market price. 3174 :param expDate: string "Undefined" by default or local date in future. 3175 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3176 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3177 A limit order has no expiration date, it lasts until the end of the trading day. 3178 :return: JSON with response from broker server. 3179 """ 3180 if self.accountId is None or not self.accountId: 3181 uLogger.error("Variable `accountId` must be defined for using this method!") 3182 raise Exception("Account ID required") 3183 3184 if operation is None or not operation or operation not in ("Buy", "Sell"): 3185 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3186 raise Exception("Incorrect value") 3187 3188 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3189 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3190 raise Exception("Incorrect value") 3191 3192 if lots is None or lots < 1: 3193 uLogger.error("You must define trade volume > 0: integer count of lots!") 3194 raise Exception("Incorrect value") 3195 3196 if targetPrice is None or targetPrice <= 0: 3197 uLogger.error("Target price for limit-order must be greater than 0!") 3198 raise Exception("Incorrect value") 3199 3200 if limitPrice is None or limitPrice <= 0: 3201 limitPrice = targetPrice 3202 3203 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3204 stopType = "Limit" 3205 3206 if expDate is None or not expDate: 3207 expDate = "Undefined" 3208 3209 if not (self._ticker or self._figi): 3210 uLogger.error("Tocker or FIGI must be defined!") 3211 raise Exception("Ticker or FIGI required") 3212 3213 response = {} 3214 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3215 self._ticker = instrument["ticker"] 3216 self._figi = instrument["figi"] 3217 3218 if orderType == "Limit": 3219 uLogger.debug( 3220 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3221 self._ticker, self._figi, 3222 operation, lots, targetPrice, instrument["currency"], 3223 )) 3224 3225 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3226 self.body = str({ 3227 "figi": self._figi, 3228 "quantity": str(lots), 3229 "price": FloatToNano(targetPrice), 3230 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3231 "accountId": str(self.accountId), 3232 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3233 }) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3235 3236 if "orderId" in response.keys(): 3237 uLogger.info( 3238 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3239 response["orderId"], self._ticker, self._figi, operation, lots, 3240 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3241 )) 3242 3243 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3244 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3245 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3246 targetPrice, instrument["currency"], 3247 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3248 )) 3249 3250 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3251 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3252 targetPrice, instrument["currency"], 3253 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3254 )) 3255 3256 else: 3257 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3258 3259 if orderType == "Stop": 3260 uLogger.debug( 3261 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3262 self._ticker, self._figi, 3263 operation, lots, 3264 targetPrice, instrument["currency"], 3265 limitPrice, instrument["currency"], 3266 stopType, expDate, 3267 )) 3268 3269 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3270 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3271 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3272 3273 body = { 3274 "figi": self._figi, 3275 "quantity": str(lots), 3276 "price": FloatToNano(limitPrice), 3277 "stopPrice": FloatToNano(targetPrice), 3278 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3279 "accountId": str(self.accountId), 3280 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3281 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3282 } 3283 3284 if expDateUTC: 3285 body["expireDate"] = expDateUTC 3286 3287 self.body = str(body) 3288 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3289 3290 if "stopOrderId" in response.keys(): 3291 uLogger.info( 3292 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3293 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3294 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3296 TKS_STOP_ORDER_TYPES[stopOrderType], 3297 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3298 )) 3299 3300 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3301 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3302 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3303 targetPrice, instrument["currency"], 3304 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3305 )) 3306 3307 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3308 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3309 targetPrice, instrument["currency"], 3310 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3311 )) 3312 3313 else: 3314 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3315 3316 return response 3317 3318 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3319 """ 3320 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3321 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3322 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3323 See also: `Order()` docstring. 3324 3325 :param lots: volume, integer count of lots >= 1. 3326 :param targetPrice: target price > 0. This is open trade price for limit order. 3327 :return: JSON with response from broker server. 3328 """ 3329 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3330 3331 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3332 """ 3333 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3334 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3335 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3336 target price value then broker opens a limit order. See also: `Order()` docstring. 3337 3338 :param lots: volume, integer count of lots >= 1. 3339 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3340 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3341 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3342 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3343 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3344 :param expDate: string "Undefined" by default or local date in future. 3345 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3346 This date is converting to UTC format for server. 3347 :return: JSON with response from broker server. 3348 """ 3349 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3350 3351 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3352 """ 3353 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3354 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3355 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3356 See also: `Order()` docstring. 3357 3358 :param lots: volume, integer count of lots >= 1. 3359 :param targetPrice: target price > 0. This is open trade price for limit order. 3360 :return: JSON with response from broker server. 3361 """ 3362 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3363 3364 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3365 """ 3366 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3367 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3368 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3369 target price value then broker opens a limit order. See also: `Order()` docstring. 3370 3371 :param lots: volume, integer count of lots >= 1. 3372 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3373 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3374 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3375 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3376 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3377 :param expDate: string "Undefined" by default or local date in future. 3378 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3379 This date is converting to UTC format for server. 3380 :return: JSON with response from broker server. 3381 """ 3382 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3383 3384 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3385 """ 3386 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3387 3388 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3389 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3390 This avoids unnecessary downloading data from the server. 3391 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3392 """ 3393 if self.accountId is None or not self.accountId: 3394 uLogger.error("Variable `accountId` must be defined for using this method!") 3395 raise Exception("Account ID required") 3396 3397 if orderIDs: 3398 if allOrdersIDs is None: 3399 rawOrders = self.RequestPendingOrders() 3400 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3401 3402 if allStopOrdersIDs is None: 3403 rawStopOrders = self.RequestStopOrders() 3404 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3405 3406 for orderID in orderIDs: 3407 idInPendingOrders = orderID in allOrdersIDs 3408 idInStopOrders = orderID in allStopOrdersIDs 3409 3410 if not (idInPendingOrders or idInStopOrders): 3411 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3412 continue 3413 3414 else: 3415 if idInPendingOrders: 3416 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3417 3418 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3419 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3420 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3421 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3422 3423 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3424 if self.moreDebug: 3425 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3426 3427 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3428 3429 else: 3430 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3431 3432 elif idInStopOrders: 3433 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3434 3435 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3436 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3437 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3438 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3439 3440 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3441 if self.moreDebug: 3442 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3443 3444 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3445 3446 else: 3447 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3448 3449 else: 3450 continue 3451 3452 def CloseAllOrders(self) -> None: 3453 """ 3454 Gets a list of open pending and stop orders and cancel it all. 3455 """ 3456 rawOrders = self.RequestPendingOrders() 3457 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3458 lenOrders = len(allOrdersIDs) 3459 3460 rawStopOrders = self.RequestStopOrders() 3461 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3462 lenSOrders = len(allStopOrdersIDs) 3463 3464 if lenOrders > 0 or lenSOrders > 0: 3465 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3466 3467 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3468 3469 else: 3470 uLogger.info("Orders not found, nothing to cancel.") 3471 3472 def CloseAll(self, *args) -> None: 3473 """ 3474 Close all available (not blocked) opened trades and orders. 3475 3476 Also, you can select one or more keywords case-insensitive: 3477 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3478 3479 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3480 """ 3481 overview = self.Overview(show=False) # get all open trades info 3482 3483 if len(args) == 0: 3484 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3485 self.CloseAllOrders() # close all pending and stop orders 3486 3487 for iType in TKS_INSTRUMENTS: 3488 if iType != "Currencies": 3489 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3490 3491 else: 3492 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3493 lowerArgs = [x.lower() for x in args] 3494 3495 if "orders" in lowerArgs: 3496 self.CloseAllOrders() # close all pending and stop orders 3497 3498 for iType in TKS_INSTRUMENTS: 3499 if iType.lower() in lowerArgs and iType != "Currencies": 3500 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3501 3502 def CloseAllByTicker(self, instrument: str) -> None: 3503 """ 3504 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3505 3506 This method searches opened trade and orders of instrument throw all portfolio and then use 3507 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3508 3509 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3510 3511 :param instrument: string with ticker. 3512 """ 3513 if instrument is None or not instrument: 3514 uLogger.error("Ticker name must be defined for using this method!") 3515 raise Exception("Ticker required") 3516 3517 overview = self.Overview(show=False) # get user portfolio with all open trades info 3518 3519 self._ticker = instrument # try to set instrument as ticker 3520 self._figi = "" 3521 3522 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3523 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3524 3525 if limitAll and self.IsInLimitOrders(portfolio=overview): 3526 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3527 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3528 3529 if stopAll and self.IsInStopOrders(portfolio=overview): 3530 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3531 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3532 3533 if self.IsInPortfolio(portfolio=overview): 3534 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3535 self.CloseTrades(instruments=[instrument], portfolio=overview) 3536 3537 def CloseAllByFIGI(self, instrument: str) -> None: 3538 """ 3539 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3540 3541 This method searches opened trade and orders of instrument throw all portfolio and then use 3542 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3543 3544 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3545 3546 :param instrument: string with FIGI id. 3547 """ 3548 if instrument is None or not instrument: 3549 uLogger.error("FIGI id must be defined for using this method!") 3550 raise Exception("FIGI required") 3551 3552 overview = self.Overview(show=False) # get user portfolio with all open trades info 3553 3554 self._ticker = "" 3555 self._figi = instrument # try to set instrument as FIGI id 3556 3557 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3558 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3559 3560 if limitAll and self.IsInLimitOrders(portfolio=overview): 3561 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3562 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3563 3564 if stopAll and self.IsInStopOrders(portfolio=overview): 3565 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3566 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3567 3568 if self.IsInPortfolio(portfolio=overview): 3569 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3570 self.CloseTrades(instruments=[instrument], portfolio=overview) 3571 3572 @staticmethod 3573 def ParseOrderParameters(operation, **inputParameters): 3574 """ 3575 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3576 3577 :param operation: string "Buy" or "Sell". 3578 :param inputParameters: this is dict of strings that looks like this 3579 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3580 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3581 "prices" key: one or more prices to open limit-orders 3582 Counts of values in lots and prices lists must be equals! 3583 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3584 """ 3585 # TODO: update order grid work with api v2 3586 pass 3587 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3588 # 3589 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3590 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3591 # raise Exception("Incorrect value") 3592 # 3593 # if "l" in inputParameters.keys(): 3594 # inputParameters["lots"] = inputParameters.pop("l") 3595 # 3596 # if "p" in inputParameters.keys(): 3597 # inputParameters["prices"] = inputParameters.pop("p") 3598 # 3599 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3600 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3601 # raise Exception("Incorrect value") 3602 # 3603 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3604 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3605 # 3606 # if len(lots) != len(prices): 3607 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3608 # raise Exception("Incorrect value") 3609 # 3610 # uLogger.debug("Extracted parameters for orders:") 3611 # uLogger.debug("lots = {}".format(lots)) 3612 # uLogger.debug("prices = {}".format(prices)) 3613 # 3614 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3615 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3616 # uLogger.debug("Order parameters: {}".format(result)) 3617 # 3618 # return result 3619 3620 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3621 """ 3622 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3623 3624 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3625 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3626 """ 3627 result = False 3628 msg = "Instrument not defined!" 3629 3630 if portfolio is None or not portfolio: 3631 portfolio = self.Overview(show=False) 3632 3633 if self._ticker: 3634 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3635 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3636 3637 for iType in TKS_INSTRUMENTS: 3638 for instrument in portfolio["stat"][iType]: 3639 if instrument["ticker"] == self._ticker: 3640 result = True 3641 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3642 break 3643 3644 elif self._figi: 3645 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3646 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3647 3648 for iType in TKS_INSTRUMENTS: 3649 for instrument in portfolio["stat"][iType]: 3650 if instrument["figi"] == self._figi: 3651 result = True 3652 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3653 break 3654 3655 else: 3656 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3657 3658 uLogger.debug(msg) 3659 3660 return result 3661 3662 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3663 """ 3664 Returns instrument from the user's portfolio if it presents there. 3665 Instrument must be defined by `ticker` (highly priority) or `figi`. 3666 3667 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3668 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3669 """ 3670 result = None 3671 msg = "Instrument not defined!" 3672 3673 if portfolio is None or not portfolio: 3674 portfolio = self.Overview(show=False) 3675 3676 if self._ticker: 3677 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3678 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3679 3680 for iType in TKS_INSTRUMENTS: 3681 for instrument in portfolio["stat"][iType]: 3682 if instrument["ticker"] == self._ticker: 3683 result = instrument 3684 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3685 break 3686 3687 elif self._figi: 3688 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3689 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["figi"] == self._figi: 3694 result = instrument 3695 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3696 break 3697 3698 else: 3699 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3700 3701 uLogger.debug(msg) 3702 3703 return result 3704 3705 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3706 """ 3707 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3708 3709 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3710 3711 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3712 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3713 """ 3714 result = False 3715 msg = "Instrument not defined!" 3716 3717 if portfolio is None or not portfolio: 3718 portfolio = self.Overview(show=False) 3719 3720 if self._ticker: 3721 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3722 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3723 3724 for instrument in portfolio["stat"]["orders"]: 3725 if instrument["ticker"] == self._ticker: 3726 result = True 3727 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3728 break 3729 3730 elif self._figi: 3731 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3732 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3733 3734 for instrument in portfolio["stat"]["orders"]: 3735 if instrument["figi"] == self._figi: 3736 result = True 3737 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3738 break 3739 3740 else: 3741 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3742 3743 uLogger.debug(msg) 3744 3745 return result 3746 3747 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3748 """ 3749 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3750 Instrument must be defined by `ticker` (highly priority) or `figi`. 3751 3752 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3753 3754 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3755 :return: list with `orderID`s of limit orders. 3756 """ 3757 result = [] 3758 msg = "Instrument not defined!" 3759 3760 if portfolio is None or not portfolio: 3761 portfolio = self.Overview(show=False) 3762 3763 if self._ticker: 3764 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3765 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3766 3767 for instrument in portfolio["stat"]["orders"]: 3768 if instrument["ticker"] == self._ticker: 3769 result.append(instrument["orderID"]) 3770 3771 if result: 3772 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3773 3774 elif self._figi: 3775 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3776 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["figi"] == self._figi: 3780 result.append(instrument["orderID"]) 3781 3782 if result: 3783 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3784 3785 else: 3786 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3787 3788 uLogger.debug(msg) 3789 3790 return result 3791 3792 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3793 """ 3794 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3795 3796 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3797 3798 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3799 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3800 """ 3801 result = False 3802 msg = "Instrument not defined!" 3803 3804 if portfolio is None or not portfolio: 3805 portfolio = self.Overview(show=False) 3806 3807 if self._ticker: 3808 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3809 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3810 3811 for instrument in portfolio["stat"]["stopOrders"]: 3812 if instrument["ticker"] == self._ticker: 3813 result = True 3814 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3815 break 3816 3817 elif self._figi: 3818 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3819 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3820 3821 for instrument in portfolio["stat"]["stopOrders"]: 3822 if instrument["figi"] == self._figi: 3823 result = True 3824 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3825 break 3826 3827 else: 3828 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3829 3830 uLogger.debug(msg) 3831 3832 return result 3833 3834 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3835 """ 3836 Returns list with all `orderID`s of opened stop orders for the instrument. 3837 Instrument must be defined by `ticker` (highly priority) or `figi`. 3838 3839 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3840 3841 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3842 :return: list with `orderID`s of stop orders. 3843 """ 3844 result = [] 3845 msg = "Instrument not defined!" 3846 3847 if portfolio is None or not portfolio: 3848 portfolio = self.Overview(show=False) 3849 3850 if self._ticker: 3851 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3852 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3853 3854 for instrument in portfolio["stat"]["stopOrders"]: 3855 if instrument["ticker"] == self._ticker: 3856 result.append(instrument["orderID"]) 3857 3858 if result: 3859 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3860 3861 elif self._figi: 3862 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3863 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["figi"] == self._figi: 3867 result.append(instrument["orderID"]) 3868 3869 if result: 3870 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3871 3872 else: 3873 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3874 3875 uLogger.debug(msg) 3876 3877 return result 3878 3879 def RequestLimits(self) -> dict: 3880 """ 3881 Method for obtaining the available funds for withdrawal for current `accountId`. 3882 3883 See also: 3884 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3885 - `OverviewLimits()` method 3886 3887 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3888 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3889 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3890 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3891 """ 3892 if self.accountId is None or not self.accountId: 3893 uLogger.error("Variable `accountId` must be defined for using this method!") 3894 raise Exception("Account ID required") 3895 3896 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3897 3898 self.body = str({"accountId": self.accountId}) 3899 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3900 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3901 3902 if self.moreDebug: 3903 uLogger.debug("Records about available funds for withdrawal successfully received") 3904 3905 return rawLimits 3906 3907 def OverviewLimits(self, show: bool = False) -> dict: 3908 """ 3909 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3910 3911 See also: `RequestLimits()`. 3912 3913 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3914 :return: dict with raw parsed data from server and some calculated statistics about it. 3915 """ 3916 if self.accountId is None or not self.accountId: 3917 uLogger.error("Variable `accountId` must be defined for using this method!") 3918 raise Exception("Account ID required") 3919 3920 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3921 3922 view = { 3923 "rawLimits": rawLimits, 3924 "limits": { # parsed data for every currency: 3925 "money": { # this is an array of portfolio currency positions 3926 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3927 }, 3928 "blocked": { # this is an array of blocked currency 3929 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3930 }, 3931 "blockedGuarantee": { # this is locked money under collateral for futures 3932 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3933 }, 3934 }, 3935 } 3936 3937 # --- Prepare text table with limits in human-readable format: 3938 if show: 3939 info = [ 3940 "# Withdrawal limits\n\n", 3941 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3942 "* **Account ID:** [{}]\n".format(self.accountId), 3943 ] 3944 3945 if view["limits"]["money"]: 3946 info.extend([ 3947 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3948 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3949 ]) 3950 3951 else: 3952 info.append("\nNo withdrawal limits\n") 3953 3954 for curr in view["limits"]["money"].keys(): 3955 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3956 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3957 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3958 3959 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3960 "[{}]".format(curr), 3961 "{:.2f}".format(view["limits"]["money"][curr]), 3962 "{:.2f}".format(availableMoney), 3963 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3964 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3965 ) 3966 3967 if curr == "rub": 3968 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3969 3970 else: 3971 info.append(infoStr) 3972 3973 infoText = "".join(info) 3974 3975 uLogger.info(infoText) 3976 3977 if self.withdrawalLimitsFile: 3978 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3979 fH.write(infoText) 3980 3981 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3982 3983 if self.useHTMLReports: 3984 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3985 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3986 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3987 3988 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3989 3990 return view 3991 3992 def RequestAccounts(self) -> dict: 3993 """ 3994 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3995 3996 See also: 3997 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3998 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3999 - `OverviewUserInfo()` method 4000 4001 :return: dict with raw data from server that contains accounts info. Example of dict: 4002 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4003 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4004 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4005 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4006 """ 4007 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4008 4009 self.body = str({}) 4010 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4011 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4012 4013 if self.moreDebug: 4014 uLogger.debug("Records about available accounts successfully received") 4015 4016 return rawAccounts 4017 4018 def RequestUserInfo(self) -> dict: 4019 """ 4020 Method for requesting common user's information. 4021 4022 See also: 4023 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4024 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4025 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4026 - `OverviewUserInfo()` method 4027 4028 :return: dict with raw data from server that contains user's information. Example of dict: 4029 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4030 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4031 """ 4032 uLogger.debug("Requesting common user's information. Wait, please...") 4033 4034 self.body = str({}) 4035 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4036 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4037 4038 if self.moreDebug: 4039 uLogger.debug("Records about current user successfully received") 4040 4041 return rawUserInfo 4042 4043 def RequestMarginStatus(self, accountId: str = None) -> dict: 4044 """ 4045 Method for requesting margin calculation for defined account ID. 4046 4047 See also: 4048 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4049 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4050 - `OverviewUserInfo()` method 4051 4052 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4053 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4054 Example of responses: 4055 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4056 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4057 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4058 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4059 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4060 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4061 """ 4062 if accountId is None or not accountId: 4063 if self.accountId is None or not self.accountId: 4064 uLogger.error("Variable `accountId` must be defined for using this method!") 4065 raise Exception("Account ID required") 4066 4067 else: 4068 accountId = self.accountId # use `self.accountId` (main ID) by default 4069 4070 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4071 4072 self.body = str({"accountId": accountId}) 4073 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4074 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4075 4076 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4077 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4078 rawMargin = {} 4079 4080 else: 4081 if self.moreDebug: 4082 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4083 4084 return rawMargin 4085 4086 def RequestTariffLimits(self) -> dict: 4087 """ 4088 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4089 4090 See also: 4091 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4092 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4093 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4094 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4095 - `OverviewUserInfo()` method 4096 4097 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4098 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4099 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4100 """ 4101 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4102 4103 self.body = str({}) 4104 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4105 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4106 4107 if self.moreDebug: 4108 uLogger.debug("Records with limits of current tariff successfully received") 4109 4110 return rawTariffLimits 4111 4112 def RequestBondCoupons(self, iJSON: dict) -> dict: 4113 """ 4114 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4115 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4116 All dates are in UTC timezone. 4117 4118 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4119 Documentation: 4120 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4121 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4122 4123 See also: `ExtendBondsData()`. 4124 4125 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4126 If raw iJSON is not data of bond then server returns an error [400] with message: 4127 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4128 :return: dictionary with bond payment calendar. Response example 4129 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4130 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4131 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4132 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4133 """ 4134 if iJSON["figi"] is None or not iJSON["figi"]: 4135 uLogger.error("FIGI must be defined for using this method!") 4136 raise Exception("FIGI required") 4137 4138 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4139 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4140 4141 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4142 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4143 self._figi, 4144 startDate, 4145 endDate, 4146 )) 4147 4148 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4149 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4150 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4151 4152 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4153 uLogger.warning("Instrument type is not bond!") 4154 4155 else: 4156 if self.moreDebug: 4157 uLogger.debug("Records about bond payment calendar successfully received") 4158 4159 return calendar 4160 4161 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4162 """ 4163 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4164 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4165 coupon yields, current yields and some statistics etc. 4166 4167 WARNING! This is too long operation if a lot of bonds requested from broker server. 4168 4169 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4170 4171 :param instruments: list of strings with tickers or FIGIs. 4172 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4173 for further used by data scientists or stock analytics. 4174 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4175 In XLSX-file and Pandas DataFrame fields mean: 4176 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4177 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4178 """ 4179 if instruments is None or not instruments: 4180 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4181 raise Exception("Ticker or FIGI required") 4182 4183 if isinstance(instruments, str): 4184 instruments = [instruments] 4185 4186 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4187 4188 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4189 4190 iCount = len(uniqueInstruments) 4191 tooLong = iCount >= 20 4192 if tooLong: 4193 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4194 4195 bonds = None 4196 for i, self._figi in enumerate(uniqueInstruments): 4197 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4198 4199 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4200 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4201 rawBond = self.SearchByFIGI(requestPrice=True) 4202 4203 # Widen raw data with UTC current time (iData["actualDateTime"]): 4204 actualDate = datetime.now(tzutc()) 4205 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4206 4207 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4208 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4209 4210 # Replace some values with human-readable: 4211 iData["nominalCurrency"] = iData["nominal"]["currency"] 4212 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4213 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4214 iData["aciCurrency"] = iData["aciValue"]["currency"] 4215 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4216 iData["issueSize"] = int(iData["issueSize"]) 4217 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4218 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4219 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4220 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4221 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4222 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4223 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4224 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4225 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4226 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4227 4228 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4229 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4230 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4231 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4232 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4233 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4234 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4235 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4236 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4237 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4238 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4239 4240 # Widen raw data with calendar data from `rawCalendar` values: 4241 calendarData = [] 4242 if "events" in iData["rawCalendar"].keys(): 4243 for item in iData["rawCalendar"]["events"]: 4244 calendarData.append({ 4245 "couponDate": item["couponDate"], 4246 "couponNumber": int(item["couponNumber"]), 4247 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4248 "payCurrency": item["payOneBond"]["currency"], 4249 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4250 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4251 "couponStartDate": item["couponStartDate"], 4252 "couponEndDate": item["couponEndDate"], 4253 "couponPeriod": item["couponPeriod"], 4254 }) 4255 4256 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4257 if "maturityDate" not in iData.keys(): 4258 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4259 4260 # Widen raw data with Coupon Rate. 4261 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4262 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4263 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4264 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4265 4266 # Widen raw data with Yield to Maturity (YTM) on current date. 4267 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4268 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4269 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4270 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4271 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4272 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4273 4274 iData["calendar"] = calendarData # adds calendar at the end 4275 4276 # Remove not used data: 4277 iData.pop("uid") 4278 iData.pop("positionUid") 4279 iData.pop("currentPrice") 4280 iData.pop("rawCalendar") 4281 4282 colNames = list(iData.keys()) 4283 if bonds is None: 4284 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4285 4286 else: 4287 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4288 4289 else: 4290 uLogger.warning("Instrument is not a bond!") 4291 4292 processed = round(100 * (i + 1) / iCount, 1) 4293 if tooLong and processed % 5 == 0: 4294 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4295 4296 else: 4297 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4298 4299 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4300 4301 # Saving bonds from Pandas DataFrame to XLSX sheet: 4302 if xlsx and self.bondsXLSXFile: 4303 with pd.ExcelWriter( 4304 path=self.bondsXLSXFile, 4305 date_format=TKS_DATE_FORMAT, 4306 datetime_format=TKS_DATE_TIME_FORMAT, 4307 mode="w", 4308 ) as writer: 4309 bonds.to_excel( 4310 writer, 4311 sheet_name="Extended bonds data", 4312 index=True, 4313 encoding="UTF-8", 4314 freeze_panes=(1, 1), 4315 ) # saving as XLSX-file with freeze first row and column as headers 4316 4317 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4318 4319 return bonds 4320 4321 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4322 """ 4323 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4324 4325 WARNING! This is too long operation if a lot of bonds requested from broker server. 4326 4327 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4328 4329 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4330 extended information about bonds: main info, current prices, bond payment calendar, 4331 coupon yields, current yields and some statistics etc. 4332 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4333 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4334 for further used by data scientists or stock analytics. 4335 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4336 """ 4337 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4338 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4339 4340 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4341 4342 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4343 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4344 calendar = None 4345 for bond in extBonds.iterrows(): 4346 for item in bond[1]["calendar"]: 4347 cData = { 4348 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4349 "couponDate": item["couponDate"], 4350 "figi": bond[1]["figi"], 4351 "ticker": bond[1]["ticker"], 4352 "name": bond[1]["name"], 4353 "couponNumber": item["couponNumber"], 4354 "payOneBond": item["payOneBond"], 4355 "payCurrency": item["payCurrency"], 4356 "couponType": item["couponType"], 4357 "couponPeriod": item["couponPeriod"], 4358 "fixDate": item["fixDate"], 4359 "couponStartDate": item["couponStartDate"], 4360 "couponEndDate": item["couponEndDate"], 4361 } 4362 4363 if calendar is None: 4364 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4365 4366 else: 4367 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4368 4369 if calendar is not None: 4370 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4371 4372 # Saving calendar from Pandas DataFrame to XLSX sheet: 4373 if xlsx: 4374 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4375 4376 with pd.ExcelWriter( 4377 path=xlsxCalendarFile, 4378 date_format=TKS_DATE_FORMAT, 4379 datetime_format=TKS_DATE_TIME_FORMAT, 4380 mode="w", 4381 ) as writer: 4382 humanReadable = calendar.copy(deep=True) 4383 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4384 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4387 humanReadable.columns = colNames # human-readable column names 4388 4389 humanReadable.to_excel( 4390 writer, 4391 sheet_name="Bond payments calendar", 4392 index=False, 4393 encoding="UTF-8", 4394 freeze_panes=(1, 2), 4395 ) # saving as XLSX-file with freeze first row and column as headers 4396 4397 del humanReadable # release df in memory 4398 4399 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4400 4401 return calendar 4402 4403 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4404 """ 4405 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4406 Also, creates Markdown file with calendar data, `calendar.md` by default. 4407 4408 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4409 4410 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4411 extended information about bonds: main info, current prices, bond payment calendar, 4412 coupon yields, current yields and some statistics etc. 4413 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4414 :param show: if `True` then also printing bonds payment calendar to the console, 4415 otherwise save to file `calendarFile` only. `False` by default. 4416 :return: multilines text in Markdown format with bonds payment calendar as a table. 4417 """ 4418 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4419 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4420 4421 infoText = "# Bond payments calendar\n\n" 4422 4423 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4424 4425 if not (calendar is None or calendar.empty): 4426 splitLine = "| | | | | | | | | |\n" 4427 4428 info = [ 4429 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4430 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4431 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4432 ] 4433 4434 newMonth = False 4435 notOneBond = calendar["figi"].nunique() > 1 4436 for i, bond in enumerate(calendar.iterrows()): 4437 if newMonth and notOneBond: 4438 info.append(splitLine) 4439 4440 info.append( 4441 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4442 " √" if bond[1]["paid"] else " —", 4443 bond[1]["couponDate"].split("T")[0], 4444 bond[1]["figi"], 4445 bond[1]["ticker"], 4446 bond[1]["couponNumber"], 4447 "{} {}".format( 4448 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4449 bond[1]["payCurrency"], 4450 ), 4451 bond[1]["couponType"], 4452 bond[1]["couponPeriod"], 4453 bond[1]["fixDate"].split("T")[0], 4454 ) 4455 ) 4456 4457 if i < len(calendar.values) - 1: 4458 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4459 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4460 newMonth = False if curDate.month == nextDate.month else True 4461 4462 else: 4463 newMonth = False 4464 4465 infoText += "".join(info) 4466 4467 if show: 4468 uLogger.info("{}".format(infoText)) 4469 4470 if self.calendarFile is not None: 4471 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4472 fH.write(infoText) 4473 4474 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4475 4476 if self.useHTMLReports: 4477 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4478 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4479 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4480 4481 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4482 4483 else: 4484 infoText += "No data\n" 4485 4486 return infoText 4487 4488 def OverviewAccounts(self, show: bool = False) -> dict: 4489 """ 4490 Method for parsing and show simple table with all available user accounts. 4491 4492 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4493 4494 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4495 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4496 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4497 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4498 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4499 "closed": "—", "access": "Full access" }, ...}}` 4500 """ 4501 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4502 4503 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4504 accounts = { 4505 item["id"]: { 4506 "type": TKS_ACCOUNT_TYPES[item["type"]], 4507 "name": item["name"], 4508 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4509 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4510 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4511 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4512 } for item in rawAccounts["accounts"] 4513 } 4514 4515 # Raw and parsed data with some fields replaced in "stat" section: 4516 view = { 4517 "rawAccounts": rawAccounts, 4518 "stat": accounts, 4519 } 4520 4521 # --- Prepare simple text table with only accounts data in human-readable format: 4522 if show: 4523 info = [ 4524 "# User accounts\n\n", 4525 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4526 "| Account ID | Type | Status | Name |\n", 4527 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4528 ] 4529 4530 for account in view["stat"].keys(): 4531 info.extend([ 4532 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4533 account, 4534 view["stat"][account]["type"], 4535 view["stat"][account]["status"], 4536 view["stat"][account]["name"], 4537 ) 4538 ]) 4539 4540 infoText = "".join(info) 4541 4542 uLogger.info(infoText) 4543 4544 if self.userAccountsFile: 4545 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4546 fH.write(infoText) 4547 4548 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4549 4550 if self.useHTMLReports: 4551 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4552 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4553 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4554 4555 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4556 4557 return view 4558 4559 def OverviewUserInfo(self, show: bool = False) -> dict: 4560 """ 4561 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4562 4563 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4564 4565 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4566 :return: dict with raw parsed data from server and some calculated statistics about it. 4567 """ 4568 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4569 tmpTicker = self._ticker 4570 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4571 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4572 self._ticker = tmpTicker 4573 4574 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4575 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4576 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4577 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4578 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4579 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4580 4581 # This is dict with parsed common user data: 4582 userInfo = { 4583 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4584 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4585 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4586 "tariff": rawUserInfo["tariff"], 4587 } 4588 4589 # This is an array of dict with parsed margin statuses for every account IDs: 4590 margins = {} 4591 for accountId in accounts.keys(): 4592 if rawMargins[accountId]: 4593 margins[accountId] = { 4594 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4595 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4596 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4597 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4598 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4599 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4600 "missing": missing["volume"], 4601 } 4602 4603 else: 4604 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4605 4606 unary = {} # unary-connection limits 4607 for item in rawTariffLimits["unaryLimits"]: 4608 if item["limitPerMinute"] in unary.keys(): 4609 unary[item["limitPerMinute"]].extend(item["methods"]) 4610 4611 else: 4612 unary[item["limitPerMinute"]] = item["methods"] 4613 4614 stream = {} # stream-connection limits 4615 for item in rawTariffLimits["streamLimits"]: 4616 if item["limit"] in stream.keys(): 4617 stream[item["limit"]].extend(item["streams"]) 4618 4619 else: 4620 stream[item["limit"]] = item["streams"] 4621 4622 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4623 limits = { 4624 "unary": unary, 4625 "stream": stream, 4626 } 4627 4628 # Raw and parsed data as an output result: 4629 view = { 4630 "rawUserInfo": rawUserInfo, 4631 "rawAccounts": rawAccounts, 4632 "rawMargins": rawMargins, 4633 "rawTariffLimits": rawTariffLimits, 4634 "stat": { 4635 "overview": overview, 4636 "userInfo": userInfo, 4637 "accounts": accounts, 4638 "margins": margins, 4639 "limits": limits, 4640 }, 4641 } 4642 4643 # --- Prepare text table with user information in human-readable format: 4644 if show: 4645 info = [ 4646 "# Full user information\n\n", 4647 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4648 "## Common information\n\n", 4649 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4650 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4651 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4652 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4653 "\n## User accounts\n\n", 4654 ] 4655 4656 for account in view["stat"]["accounts"].keys(): 4657 info.extend([ 4658 "### ID: [{}]\n\n".format(account), 4659 "| Parameters | Values |\n", 4660 "|----------------------|--------------------------------------------------------------|\n", 4661 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4662 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4663 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4664 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4665 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4666 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4667 ]) 4668 4669 if margins[account]: 4670 info.extend([ 4671 "| Margin status: | Enabled |\n", 4672 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4673 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4674 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4675 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4676 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4677 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4678 ]) 4679 4680 else: 4681 info.append("| Margin status: | Disabled |\n\n") 4682 4683 info.extend([ 4684 "\n## Current user tariff limits\n", 4685 "\n### See also\n", 4686 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4687 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4688 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4689 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4690 "\n### Unary limits\n", 4691 ]) 4692 4693 if unary: 4694 for key, values in sorted(unary.items()): 4695 info.append("\n* Max requests per minute: {}\n".format(key)) 4696 4697 for value in values: 4698 info.append(" - {}\n".format(value)) 4699 4700 else: 4701 info.append("\nNot available\n") 4702 4703 info.append("\n### Stream limits\n") 4704 4705 if stream: 4706 for key, values in sorted(stream.items()): 4707 info.append("\n* Max stream connections: {}\n".format(key)) 4708 4709 for value in values: 4710 info.append(" - {}\n".format(value)) 4711 4712 else: 4713 info.append("\nNot available\n") 4714 4715 infoText = "".join(info) 4716 4717 uLogger.info(infoText) 4718 4719 if self.userInfoFile: 4720 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4721 fH.write(infoText) 4722 4723 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4724 4725 if self.useHTMLReports: 4726 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4727 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4728 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4729 4730 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4731 4732 return view 4733 4734 4735class Args: 4736 """ 4737 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4738 """ 4739 def __init__(self, **kwargs): 4740 self.__dict__.update(kwargs) 4741 4742 def __getattr__(self, item): 4743 return None 4744 4745 4746def ParseArgs(): 4747 """This function get and parse command line keys.""" 4748 parser = ArgumentParser() # command-line string parser 4749 4750 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4751 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4752 4753 # --- options: 4754 4755 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4756 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4757 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4758 4759 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4760 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4761 4762 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4763 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4764 4765 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4766 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4767 4768 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4769 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4770 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4771 4772 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4773 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4774 4775 # --- commands: 4776 4777 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4778 4779 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4780 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4781 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4782 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4783 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4784 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4785 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4786 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4787 4788 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4789 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4790 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4791 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4792 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4793 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4794 4795 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4796 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4797 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4798 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4799 4800 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4801 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4802 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4803 4804 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4805 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4806 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4807 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4808 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4809 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4810 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4811 4812 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4813 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4814 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4815 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4816 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4817 4818 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4819 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4820 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4821 4822 cmdArgs = parser.parse_args() 4823 return cmdArgs 4824 4825 4826def Main(**kwargs): 4827 """ 4828 Main function for work with TKSBrokerAPI in the console. 4829 4830 See examples: 4831 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4832 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4833 """ 4834 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4835 4836 if args.debug_level: 4837 uLogger.level = 10 # always debug level by default 4838 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4839 4840 exitCode = 0 4841 start = datetime.now(tzutc()) 4842 uLogger.debug("=-" * 50) 4843 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4844 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4845 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4846 )) 4847 4848 # trying to calculate full current version: 4849 buildVersion = __version__ 4850 try: 4851 v = version("tksbrokerapi") 4852 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4853 4854 except Exception: 4855 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4856 4857 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4858 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4859 4860 try: 4861 if args.version: 4862 print("TKSBrokerAPI {}".format(buildVersion)) 4863 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4864 4865 else: 4866 # Init class for trading with Tinkoff Broker: 4867 trader = TinkoffBrokerServer( 4868 token=args.token, 4869 accountId=args.account_id, 4870 useCache=not args.no_cache, 4871 ) 4872 4873 # --- set some options: 4874 4875 if args.more: 4876 trader.moreDebug = True 4877 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4878 4879 if args.html: 4880 trader.useHTMLReports = True 4881 4882 if args.ticker: 4883 ticker = str(args.ticker).upper() # Tickers may be upper case only 4884 4885 if ticker in trader.aliasesKeys: 4886 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4887 4888 else: 4889 trader.ticker = ticker 4890 4891 if args.figi: 4892 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4893 4894 if args.depth is not None: 4895 trader.depth = args.depth 4896 4897 # --- do one command: 4898 4899 if args.list: 4900 if args.output is not None: 4901 trader.instrumentsFile = args.output 4902 4903 trader.ShowInstrumentsInfo(show=True) 4904 4905 elif args.list_xlsx: 4906 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4907 4908 elif args.bonds_xlsx is not None: 4909 if args.output is not None: 4910 trader.bondsXLSXFile = args.output 4911 4912 if len(args.bonds_xlsx) == 0: 4913 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4914 4915 else: 4916 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4917 4918 elif args.search: 4919 if args.output is not None: 4920 trader.searchResultsFile = args.output 4921 4922 trader.SearchInstruments(pattern=args.search[0], show=True) 4923 4924 elif args.info: 4925 if not (args.ticker or args.figi): 4926 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4927 raise Exception("Ticker or FIGI required") 4928 4929 if args.output is not None: 4930 trader.infoFile = args.output 4931 4932 if args.ticker: 4933 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4934 4935 else: 4936 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4937 4938 elif args.calendar is not None: 4939 if args.output is not None: 4940 trader.calendarFile = args.output 4941 4942 if len(args.calendar) == 0: 4943 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4944 4945 else: 4946 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4947 4948 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4949 4950 elif args.price: 4951 if not (args.ticker or args.figi): 4952 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4953 raise Exception("Ticker or FIGI required") 4954 4955 trader.GetCurrentPrices(show=True) 4956 4957 elif args.prices is not None: 4958 if args.output is not None: 4959 trader.pricesFile = args.output 4960 4961 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4962 4963 elif args.overview: 4964 if args.output is not None: 4965 trader.overviewFile = args.output 4966 4967 trader.Overview(show=True, details="full") 4968 4969 elif args.overview_digest: 4970 if args.output is not None: 4971 trader.overviewDigestFile = args.output 4972 4973 trader.Overview(show=True, details="digest") 4974 4975 elif args.overview_positions: 4976 if args.output is not None: 4977 trader.overviewPositionsFile = args.output 4978 4979 trader.Overview(show=True, details="positions") 4980 4981 elif args.overview_orders: 4982 if args.output is not None: 4983 trader.overviewOrdersFile = args.output 4984 4985 trader.Overview(show=True, details="orders") 4986 4987 elif args.overview_analytics: 4988 if args.output is not None: 4989 trader.overviewAnalyticsFile = args.output 4990 4991 trader.Overview(show=True, details="analytics") 4992 4993 elif args.overview_calendar: 4994 if args.output is not None: 4995 trader.overviewAnalyticsFile = args.output 4996 4997 trader.Overview(show=True, details="calendar") 4998 4999 elif args.deals is not None: 5000 if args.output is not None: 5001 trader.reportFile = args.output 5002 5003 if 0 <= len(args.deals) < 3: 5004 trader.Deals( 5005 start=args.deals[0] if len(args.deals) >= 1 else None, 5006 end=args.deals[1] if len(args.deals) == 2 else None, 5007 show=True, # Always show deals report in console 5008 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5009 ) 5010 5011 else: 5012 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5013 raise Exception("Incorrect value") 5014 5015 elif args.history is not None: 5016 if args.output is not None: 5017 trader.historyFile = args.output 5018 5019 if 0 <= len(args.history) < 3: 5020 dataReceived = trader.History( 5021 start=args.history[0] if len(args.history) >= 1 else None, 5022 end=args.history[1] if len(args.history) == 2 else None, 5023 interval="hour" if args.interval is None or not args.interval else args.interval, 5024 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5025 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5026 show=True, # shows all downloaded candles in console 5027 ) 5028 5029 if args.render_chart is not None and dataReceived is not None: 5030 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5031 5032 trader.ShowHistoryChart( 5033 candles=dataReceived, 5034 interact=iChart, 5035 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5036 ) 5037 5038 else: 5039 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5040 raise Exception("Incorrect value") 5041 5042 elif args.load_history is not None: 5043 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5044 5045 if args.render_chart is not None and histData is not None: 5046 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5047 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5048 5049 trader.ShowHistoryChart( 5050 candles=histData, 5051 interact=iChart, 5052 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5053 ) 5054 5055 elif args.trade is not None: 5056 if 1 <= len(args.trade) <= 5: 5057 trader.Trade( 5058 operation=args.trade[0], 5059 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5060 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5061 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5062 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5063 ) 5064 5065 else: 5066 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5067 5068 elif args.buy is not None: 5069 if 0 <= len(args.buy) <= 4: 5070 trader.Buy( 5071 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5072 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5073 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5074 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5075 ) 5076 5077 else: 5078 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5079 5080 elif args.sell is not None: 5081 if 0 <= len(args.sell) <= 4: 5082 trader.Sell( 5083 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5084 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5085 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5086 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5087 ) 5088 5089 else: 5090 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5091 5092 elif args.order: 5093 if 4 <= len(args.order) <= 7: 5094 trader.Order( 5095 operation=args.order[0], 5096 orderType=args.order[1], 5097 lots=int(args.order[2]), 5098 targetPrice=float(args.order[3]), 5099 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5100 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5101 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5102 ) 5103 5104 else: 5105 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5106 5107 elif args.buy_limit: 5108 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5109 5110 elif args.sell_limit: 5111 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5112 5113 elif args.buy_stop: 5114 if 2 <= len(args.buy_stop) <= 7: 5115 trader.BuyStop( 5116 lots=int(args.buy_stop[0]), 5117 targetPrice=float(args.buy_stop[1]), 5118 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5119 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5120 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5121 ) 5122 5123 else: 5124 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5125 5126 elif args.sell_stop: 5127 if 2 <= len(args.sell_stop) <= 7: 5128 trader.SellStop( 5129 lots=int(args.sell_stop[0]), 5130 targetPrice=float(args.sell_stop[1]), 5131 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5132 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5133 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5134 ) 5135 5136 else: 5137 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5138 5139 # elif args.buy_order_grid is not None: 5140 # # update order grid work with api v2 5141 # if len(args.buy_order_grid) == 2: 5142 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5143 # 5144 # for order in orderParams: 5145 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5146 # 5147 # else: 5148 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5149 # 5150 # elif args.sell_order_grid is not None: 5151 # # update order grid work with api v2 5152 # if len(args.sell_order_grid) >= 2: 5153 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5154 # 5155 # for order in orderParams: 5156 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5157 # 5158 # else: 5159 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5160 5161 elif args.close_order is not None: 5162 trader.CloseOrders(args.close_order) # close only one order 5163 5164 elif args.close_orders is not None: 5165 trader.CloseOrders(args.close_orders) # close list of orders 5166 5167 elif args.close_trade: 5168 if not (args.ticker or args.figi): 5169 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5170 raise Exception("Ticker or FIGI required") 5171 5172 if args.ticker: 5173 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5174 5175 else: 5176 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5177 5178 elif args.close_trades is not None: 5179 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5180 5181 elif args.close_all is not None: 5182 if args.ticker: 5183 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5184 5185 elif args.figi: 5186 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5187 5188 else: 5189 trader.CloseAll(*args.close_all) 5190 5191 elif args.limits: 5192 if args.output is not None: 5193 trader.withdrawalLimitsFile = args.output 5194 5195 trader.OverviewLimits(show=True) 5196 5197 elif args.user_info: 5198 if args.output is not None: 5199 trader.userInfoFile = args.output 5200 5201 trader.OverviewUserInfo(show=True) 5202 5203 elif args.account: 5204 if args.output is not None: 5205 trader.userAccountsFile = args.output 5206 5207 trader.OverviewAccounts(show=True) 5208 5209 else: 5210 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5211 raise Exception("There is no command to execute") 5212 5213 except Exception: 5214 trace = tb.format_exc() 5215 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5216 if e in trace: 5217 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5218 break 5219 5220 uLogger.debug(trace) 5221 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5222 exitCode = 255 # an error occurred, must be open a ticket for this issue 5223 5224 finally: 5225 finish = datetime.now(tzutc()) 5226 5227 if exitCode == 0: 5228 if args.more: 5229 uLogger.debug("All operations were finished success (summary code is 0).") 5230 5231 else: 5232 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5233 os.path.abspath(uLog.defaultLogFile), exitCode, 5234 )) 5235 5236 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5237 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5238 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5239 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5240 )) 5241 uLogger.debug("=-" * 50) 5242 5243 if not kwargs: 5244 sys.exit(exitCode) 5245 5246 else: 5247 return exitCode 5248 5249 5250if __name__ == "__main__": 5251 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 405 """ 406 try: 407 responseJSON = json.loads(rawData) if rawData else {} 408 409 if self.moreDebug: 410 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 411 412 return responseJSON 413 414 except Exception as e: 415 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 416 417 return {} 418 419 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 420 """ 421 Send GET or POST request to broker server and receive JSON object. 422 423 self.header: must be defining with dictionary of headers. 424 self.body: if define then used as request body. None by default. 425 self.timeout: global request timeout, 15 seconds by default. 426 :param url: url with REST request. 427 :param reqType: send "GET" or "POST" request. "GET" by default. 428 :param retry: how many times retry after first request if an 5xx server errors occurred. 429 :param pause: sleep time in seconds between retries. 430 :return: response JSON (dictionary) from broker. 431 """ 432 if reqType.upper() not in ("GET", "POST"): 433 uLogger.error("You can define request type: `GET` or `POST`!") 434 raise Exception("Incorrect value") 435 436 if self.moreDebug: 437 uLogger.debug("Request parameters:") 438 uLogger.debug(" - REST API URL: {}".format(url)) 439 uLogger.debug(" - request type: {}".format(reqType)) 440 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 441 uLogger.debug(" - body:\n{}".format(self.body)) 442 443 # fast hack to avoid all operations with some tickers/FIGI 444 responseJSON = {} 445 oK = True 446 for item in self.exclude: 447 if item in url: 448 if self.moreDebug: 449 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 450 451 oK = False 452 break 453 454 if oK: 455 with self.__lock: # acquire the mutex lock 456 counter = 0 457 response = None 458 errMsg = "" 459 460 while not response and counter <= retry: 461 if reqType == "GET": 462 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 463 464 if reqType == "POST": 465 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 466 467 if self.moreDebug: 468 uLogger.debug("Response:") 469 uLogger.debug(" - status code: {}".format(response.status_code)) 470 uLogger.debug(" - reason: {}".format(response.reason)) 471 uLogger.debug(" - body length: {}".format(len(response.text))) 472 uLogger.debug(" - headers:\n{}".format(response.headers)) 473 474 # Server returns some headers: 475 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 476 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 477 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 478 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 479 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 480 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 481 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 482 sleep(rateLimitWait) 483 484 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 485 if 400 <= response.status_code < 500: 486 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 487 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 488 489 if "code" in response.text and "message" in response.text: 490 msgDict = self._ParseJSON(rawData=response.text) 491 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 492 493 counter = retry + 1 # do not retry for 4xx errors 494 495 if 500 <= response.status_code < 600: 496 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 497 uLogger.debug(" - not oK, {}".format(errMsg)) 498 499 if "code" in response.text and "message" in response.text: 500 errMsgDict = self._ParseJSON(rawData=response.text) 501 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 502 503 counter += 1 504 505 if counter <= retry: 506 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 507 sleep(pause) 508 509 responseJSON = self._ParseJSON(rawData=response.text) 510 511 if errMsg: 512 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 513 uLogger.error(" - not oK, {}".format(errMsg)) 514 515 return responseJSON 516 517 def _IUpdater(self, iType: str) -> tuple: 518 """ 519 Request instrument by type from server. See available API methods for instruments: 520 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 521 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 522 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 523 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 524 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 525 526 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 527 :return: tuple with iType name and list of available instruments of current type for defined user token. 528 """ 529 result = [] 530 531 if iType in TKS_INSTRUMENTS: 532 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 533 534 # all instruments have the same body in API v2 requests: 535 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 536 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 537 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 538 539 return iType, result 540 541 def _IWrapper(self, kwargs): 542 """ 543 Wrapper runs instrument's update method `_IUpdater()`. 544 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 545 """ 546 return self._IUpdater(**kwargs) 547 548 def Listing(self) -> dict: 549 """ 550 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 551 552 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 553 """ 554 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 555 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 556 557 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 558 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 559 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 560 561 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 562 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 563 poolUpdater.close() # close the thread pool 564 poolUpdater.join() # wait a moment until all data returns from threads 565 566 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 567 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 568 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 569 570 # calculate minimum price increment (step) for all instruments and set up instrument's type: 571 for iType in iList.keys(): 572 for ticker in iList[iType]: 573 iList[iType][ticker]["type"] = iType 574 575 if "minPriceIncrement" in iList[iType][ticker].keys(): 576 iList[iType][ticker]["step"] = NanoToFloat( 577 iList[iType][ticker]["minPriceIncrement"]["units"], 578 iList[iType][ticker]["minPriceIncrement"]["nano"], 579 ) 580 581 else: 582 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 583 584 return iList 585 586 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 587 """ 588 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 589 590 See also: `DumpInstruments()`, `Listing()`. 591 592 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 593 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 594 """ 595 if self.iListDumpFile is None or not self.iListDumpFile: 596 uLogger.error("Output name of dump file must be defined!") 597 raise Exception("Filename required") 598 599 if not self.iList or forceUpdate: 600 self.iList = self.Listing() 601 602 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 603 604 # Save as XLSX with separated sheets for every type of instruments: 605 with pd.ExcelWriter( 606 path=xlsxDumpFile, 607 date_format=TKS_DATE_FORMAT, 608 datetime_format=TKS_DATE_TIME_FORMAT, 609 mode="w", 610 ) as writer: 611 for iType in TKS_INSTRUMENTS: 612 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 613 df = df[sorted(df)] # sorted by column names 614 df = df.applymap( 615 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 616 na_action="ignore", 617 ) # converting numbers from nano-type to float in every cell 618 df.to_excel( 619 writer, 620 sheet_name=iType, 621 encoding="UTF-8", 622 freeze_panes=(1, 1), 623 ) # saving as XLSX-file with freeze first row and column as headers 624 625 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 626 627 def DumpInstruments(self, forceUpdate: bool = True) -> str: 628 """ 629 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 630 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 631 632 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 636 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 646 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 647 fH.write(jsonDump) 648 649 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 650 651 return jsonDump 652 653 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 654 """ 655 Show information about one instrument defined by json data and prints it in Markdown format. 656 657 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 658 659 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 660 :param show: if `True` then also printing information about instrument and its current price. 661 :return: multilines text in Markdown format with information about one instrument. 662 """ 663 splitLine = "| | |\n" 664 infoText = "" 665 666 if iJSON is not None and iJSON and isinstance(iJSON, dict): 667 info = [ 668 "# Main information\n\n", 669 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 670 "| Parameters | Values |\n", 671 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 672 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 673 "| Full name: | {:<54} |\n".format(iJSON["name"]), 674 ] 675 676 if "sector" in iJSON.keys() and iJSON["sector"]: 677 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 678 679 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 680 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 681 682 info.extend([ 683 splitLine, 684 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 685 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 686 ]) 687 688 if "isin" in iJSON.keys() and iJSON["isin"]: 689 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 690 691 if "classCode" in iJSON.keys(): 692 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 693 694 info.extend([ 695 splitLine, 696 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 697 splitLine, 698 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 699 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 700 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 701 ]) 702 703 if iJSON["figi"]: 704 self._figi = iJSON["figi"] 705 iJSON = iJSON | self.RequestTradingStatus() 706 707 info.extend([ 708 splitLine, 709 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 710 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 711 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 712 ]) 713 714 info.append(splitLine) 715 716 if "type" in iJSON.keys() and iJSON["type"]: 717 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 718 719 if "shareType" in iJSON.keys() and iJSON["shareType"]: 720 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 721 722 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 723 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 724 725 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 726 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 727 728 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 729 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 730 731 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 732 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 733 734 if "focusType" in iJSON.keys() and iJSON["focusType"]: 735 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 736 737 if "assetType" in iJSON.keys() and iJSON["assetType"]: 738 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 739 740 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 741 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 742 743 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 744 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 745 746 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 747 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 748 749 if "currency" in iJSON.keys(): 750 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 751 752 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 753 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 754 755 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 756 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 757 758 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 759 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 760 761 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 762 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 763 764 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 765 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 766 767 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 768 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 769 770 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 771 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 772 773 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 774 info.append("| Perpetual bond: | Yes |\n") 775 776 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 777 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 778 779 iExt = None 780 if iJSON["type"] == "Bonds": 781 info.extend([ 782 splitLine, 783 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 784 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 785 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 786 iJSON["nominal"]["currency"], 787 )), 788 ]) 789 790 if "floatingCouponFlag" in iJSON.keys(): 791 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 792 793 if "amortizationFlag" in iJSON.keys(): 794 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 795 796 info.append(splitLine) 797 798 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 799 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 800 801 if iJSON["figi"]: 802 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 803 804 info.extend([ 805 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 806 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 807 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 808 ]) 809 810 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 811 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 812 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 813 iJSON["aciValue"]["currency"] 814 ))) 815 816 if "currentPrice" in iJSON.keys(): 817 info.append(splitLine) 818 819 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 820 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 821 822 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 823 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 824 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 825 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 826 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 827 828 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 829 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 830 831 info.extend([ 832 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 833 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 834 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 835 )), 836 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 837 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 838 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 839 )), 840 "| Changes between last deal price and last close | {:<54} |\n".format( 841 "{:.2f}%{}".format( 842 iJSON["currentPrice"]["changes"], 843 " ({}{:.2f} {})".format( 844 "+" if bondChangesDelta > 0 else "", 845 bondChangesDelta, 846 aciCurrency 847 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 848 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 849 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 850 currency 851 ), 852 ) 853 ), 854 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 855 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 860 )), 861 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 862 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 863 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 864 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 865 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 866 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 867 )), 868 ]) 869 870 if "lot" in iJSON.keys(): 871 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 872 873 if "step" in iJSON.keys() and iJSON["step"] != 0: 874 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 875 876 # Add bond payment calendar: 877 if iJSON["type"] == "Bonds": 878 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 879 info.extend(["\n#", strCalendar]) 880 881 infoText += "".join(info) 882 883 if show: 884 uLogger.info("{}".format(infoText)) 885 886 else: 887 uLogger.debug("{}".format(infoText)) 888 889 if self.infoFile is not None: 890 with open(self.infoFile, "w", encoding="UTF-8") as fH: 891 fH.write(infoText) 892 893 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 894 895 if self.useHTMLReports: 896 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 897 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 898 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 899 900 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 901 902 return infoText 903 904 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 905 """ 906 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 907 908 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 909 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 910 :return: JSON formatted data with information about instrument. 911 """ 912 tickerJSON = {} 913 if self.moreDebug: 914 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 915 916 if not self._ticker: 917 uLogger.warning("self._ticker variable is not be empty!") 918 919 else: 920 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 921 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 922 raise Exception("Instrument not allowed") 923 924 if not self.iList: 925 self.iList = self.Listing() 926 927 if self._ticker in self.iList["Shares"].keys(): 928 tickerJSON = self.iList["Shares"][self._ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 931 932 elif self._ticker in self.iList["Currencies"].keys(): 933 tickerJSON = self.iList["Currencies"][self._ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 936 937 elif self._ticker in self.iList["Bonds"].keys(): 938 tickerJSON = self.iList["Bonds"][self._ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 941 942 elif self._ticker in self.iList["Etfs"].keys(): 943 tickerJSON = self.iList["Etfs"][self._ticker] 944 if self.moreDebug: 945 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 946 947 elif self._ticker in self.iList["Futures"].keys(): 948 tickerJSON = self.iList["Futures"][self._ticker] 949 if self.moreDebug: 950 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 951 952 if tickerJSON: 953 self._figi = tickerJSON["figi"] 954 955 if requestPrice: 956 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 957 958 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 959 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 960 961 else: 962 tickerJSON["currentPrice"]["changes"] = 0 963 964 if show: 965 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 966 967 else: 968 if show: 969 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 970 971 return tickerJSON 972 973 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 974 """ 975 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 976 977 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 978 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 979 :return: JSON formatted data with information about instrument. 980 """ 981 figiJSON = {} 982 if self.moreDebug: 983 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 984 985 if not self._figi: 986 uLogger.warning("self._figi variable is not be empty!") 987 988 else: 989 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 990 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 991 raise Exception("Instrument not allowed") 992 993 if not self.iList: 994 self.iList = self.Listing() 995 996 for item in self.iList["Shares"].keys(): 997 if self._figi == self.iList["Shares"][item]["figi"]: 998 figiJSON = self.iList["Shares"][item] 999 1000 if self.moreDebug: 1001 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1002 1003 break 1004 1005 if not figiJSON: 1006 for item in self.iList["Currencies"].keys(): 1007 if self._figi == self.iList["Currencies"][item]["figi"]: 1008 figiJSON = self.iList["Currencies"][item] 1009 1010 if self.moreDebug: 1011 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1012 1013 break 1014 1015 if not figiJSON: 1016 for item in self.iList["Bonds"].keys(): 1017 if self._figi == self.iList["Bonds"][item]["figi"]: 1018 figiJSON = self.iList["Bonds"][item] 1019 1020 if self.moreDebug: 1021 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1022 1023 break 1024 1025 if not figiJSON: 1026 for item in self.iList["Etfs"].keys(): 1027 if self._figi == self.iList["Etfs"][item]["figi"]: 1028 figiJSON = self.iList["Etfs"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Futures"].keys(): 1037 if self._figi == self.iList["Futures"][item]["figi"]: 1038 figiJSON = self.iList["Futures"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1042 1043 break 1044 1045 if figiJSON: 1046 self._figi = figiJSON["figi"] 1047 self._ticker = figiJSON["ticker"] 1048 1049 if requestPrice: 1050 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1051 1052 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1053 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1054 1055 else: 1056 figiJSON["currentPrice"]["changes"] = 0 1057 1058 if show: 1059 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1060 1061 else: 1062 if show: 1063 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1064 1065 return figiJSON 1066 1067 def GetCurrentPrices(self, show: bool = True) -> dict: 1068 """ 1069 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1070 `{"buy": [{"price": 1243.8, "quantity": 193}, 1071 {"price": 1244.0, "quantity": 168}, 1072 {"price": 1244.8, "quantity": 5}, 1073 {"price": 1245.0, "quantity": 61}, 1074 {"price": 1245.4, "quantity": 60}], 1075 "sell": [{"price": 1243.6, "quantity": 8}, 1076 {"price": 1242.6, "quantity": 10}, 1077 {"price": 1242.4, "quantity": 18}, 1078 {"price": 1242.2, "quantity": 50}, 1079 {"price": 1242.0, "quantity": 113}], 1080 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1081 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1082 - sell: list of dicts with Buyers prices, 1083 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1084 - quantity: volume value by current price in lots, 1085 - limitUp: current trade session limit price, maximum, 1086 - limitDown: current trade session limit price, minimum, 1087 - lastPrice: last deal price of the instrument, 1088 - closePrice: previous trade session close price of the instrument. 1089 1090 See also: `SearchByTicker()` and `SearchByFIGI()`. 1091 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1092 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1093 1094 :param show: if `True` then print DOM to log and console. 1095 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1096 If an error occurred then returns an empty record: 1097 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1098 """ 1099 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1100 1101 if self.depth < 1: 1102 uLogger.error("Depth of Market (DOM) must be >=1!") 1103 raise Exception("Incorrect value") 1104 1105 if not (self._ticker or self._figi): 1106 uLogger.error("self._ticker or self._figi variables must be defined!") 1107 raise Exception("Ticker or FIGI required") 1108 1109 if self._ticker and not self._figi: 1110 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1111 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1112 1113 if not self._ticker and self._figi: 1114 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1115 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1116 1117 if not self._figi: 1118 uLogger.error("FIGI is not defined!") 1119 raise Exception("Ticker or FIGI required") 1120 1121 else: 1122 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1123 1124 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1125 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1126 self.body = str({"figi": self._figi, "depth": self.depth}) 1127 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1128 1129 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1130 # list of dicts with sellers orders: 1131 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1132 1133 # list of dicts with buyers orders: 1134 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1135 1136 # max price of instrument at this time: 1137 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1138 1139 # min price of instrument at this time: 1140 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1141 1142 # last price of deal with instrument: 1143 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1144 1145 # last close price of instrument: 1146 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1147 1148 else: 1149 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1150 uLogger.debug("Server response: {}".format(pricesResponse)) 1151 1152 if show: 1153 if prices["buy"] or prices["sell"]: 1154 info = [ 1155 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1156 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1157 self._ticker, 1158 self._figi, 1159 self.depth, 1160 ), 1161 "-" * 60, "\n", 1162 " Orders of Buyers | Orders of Sellers\n", 1163 "-" * 60, "\n", 1164 " Sell prices (volumes) | Buy prices (volumes)\n", 1165 "-" * 60, "\n", 1166 ] 1167 1168 if not prices["buy"]: 1169 info.append(" | No orders!\n") 1170 sumBuy = 0 1171 1172 else: 1173 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1174 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1175 for item in maxMinSorted: 1176 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1177 1178 if not prices["sell"]: 1179 info.append("No orders! |\n") 1180 sumSell = 0 1181 1182 else: 1183 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1184 for item in prices["sell"]: 1185 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1186 1187 info.extend([ 1188 "-" * 60, "\n", 1189 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1190 "-" * 60, "\n", 1191 ]) 1192 1193 infoText = "".join(info) 1194 1195 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1196 1197 else: 1198 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1199 1200 return prices 1201 1202 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1203 """ 1204 This method get and show information about all available broker instruments for current user account. 1205 If `instrumentsFile` string is not empty then also save information to this file. 1206 1207 :param show: if `True` then print results to console, if `False` — print only to file. 1208 :return: multi-lines string with all available broker instruments 1209 """ 1210 if not self.iList: 1211 self.iList = self.Listing() 1212 1213 info = [ 1214 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1215 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1216 ] 1217 1218 # add instruments count by type: 1219 for iType in self.iList.keys(): 1220 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1221 1222 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1223 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1224 1225 # generating info tables with all instruments by type: 1226 for iType in self.iList.keys(): 1227 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1228 1229 for instrument in self.iList[iType].keys(): 1230 iName = self.iList[iType][instrument]["name"] # instrument's name 1231 if len(iName) > 57: 1232 iName = "{}...".format(iName[:54]) # right trim for a long string 1233 1234 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1235 self.iList[iType][instrument]["ticker"], 1236 iName, 1237 self.iList[iType][instrument]["figi"], 1238 self.iList[iType][instrument]["currency"], 1239 self.iList[iType][instrument]["lot"], 1240 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1241 )) 1242 1243 infoText = "".join(info) 1244 1245 if show: 1246 uLogger.info(infoText) 1247 1248 if self.instrumentsFile: 1249 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1250 fH.write(infoText) 1251 1252 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1253 1254 if self.useHTMLReports: 1255 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1256 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1257 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1258 1259 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1260 1261 return infoText 1262 1263 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1264 """ 1265 This method search and show information about instruments by part of its ticker, FIGI or name. 1266 If `searchResultsFile` string is not empty then also save information to this file. 1267 1268 :param pattern: string with part of ticker, FIGI or instrument's name. 1269 :param show: if `True` then print results to console, if `False` — return list of result only. 1270 :return: list of dictionaries with all found instruments. 1271 """ 1272 if not self.iList: 1273 self.iList = self.Listing() 1274 1275 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1276 compiledPattern = re.compile(pattern, re.IGNORECASE) 1277 1278 for iType in self.iList: 1279 for instrument in self.iList[iType].values(): 1280 searchResult = compiledPattern.search(" ".join( 1281 [instrument["ticker"], instrument["figi"], instrument["name"]] 1282 )) 1283 1284 if searchResult: 1285 searchResults[iType][instrument["ticker"]] = instrument 1286 1287 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1288 info = [ 1289 "# Search results\n\n", 1290 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile: 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 if self.useHTMLReports: 1342 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1343 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1344 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1345 1346 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1347 1348 return searchResults 1349 1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self._ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self._ticker = "" 1380 self._figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self._figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs 1394 1395 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 1399 See limits: https://tinkoff.github.io/investAPI/limits/ 1400 1401 If `pricesFile` string is not empty then also save information to this file. 1402 1403 :param instruments: list of strings with tickers or FIGIs. 1404 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1405 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1406 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1407 """ 1408 if instruments is None or not instruments: 1409 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1410 raise Exception("Ticker or FIGI required") 1411 1412 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1413 1414 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1415 1416 iList = [] # trying to get info and current prices about all unique instruments: 1417 for self._figi in onlyUniqueFIGIs: 1418 iData = self.SearchByFIGI(requestPrice=True) 1419 iList.append(iData) 1420 1421 self.ShowListOfPrices(iList, show) 1422 1423 return iList 1424 1425 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1426 """ 1427 Show table contains current prices of given instruments. 1428 1429 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1430 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1431 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1432 :return: multilines text in Markdown format as a table contains current prices. 1433 """ 1434 infoText = "" 1435 1436 if show or self.pricesFile: 1437 info = [ 1438 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1439 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1440 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1441 ] 1442 1443 for item in iList: 1444 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1445 item["ticker"], 1446 item["figi"], 1447 item["type"], 1448 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1449 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1450 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1451 "{} / {}".format( 1452 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1453 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1454 ), 1455 "{} / {}".format( 1456 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1457 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1458 ), 1459 item["currency"], 1460 )) 1461 1462 infoText = "".join(info) 1463 1464 if show: 1465 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1466 1467 if self.pricesFile: 1468 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1469 fH.write(infoText) 1470 1471 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1472 1473 if self.useHTMLReports: 1474 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1475 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1476 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1477 1478 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1479 1480 return infoText 1481 1482 def RequestTradingStatus(self) -> dict: 1483 """ 1484 Requesting trading status for the instrument defined by `figi` variable. 1485 1486 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1487 1488 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1489 1490 :return: dictionary with trading status attributes. Response example: 1491 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1492 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1493 """ 1494 if self._figi is None or not self._figi: 1495 uLogger.error("Variable `figi` must be defined for using this method!") 1496 raise Exception("FIGI required") 1497 1498 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1499 1500 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1501 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1502 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1503 1504 if self.moreDebug: 1505 uLogger.debug("Records about current trading status successfully received") 1506 1507 return tradingStatus 1508 1509 def RequestPortfolio(self) -> dict: 1510 """ 1511 Requesting actual user's portfolio for current `accountId`. 1512 1513 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1514 1515 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1516 1517 :return: dictionary with user's portfolio. 1518 """ 1519 if self.accountId is None or not self.accountId: 1520 uLogger.error("Variable `accountId` must be defined for using this method!") 1521 raise Exception("Account ID required") 1522 1523 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1524 1525 self.body = str({"accountId": self.accountId}) 1526 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1527 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1528 1529 if self.moreDebug: 1530 uLogger.debug("Records about user's portfolio successfully received") 1531 1532 return rawPortfolio 1533 1534 def RequestPositions(self) -> dict: 1535 """ 1536 Requesting open positions by currencies and instruments for current `accountId`. 1537 1538 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1539 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions 1558 1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending limit orders for current `accountId`. 1562 1563 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1564 1565 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1566 1567 :return: list of dictionaries with pending limit orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1577 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1578 1579 if "orders" in rawResponse.keys(): 1580 rawOrders = rawResponse["orders"] 1581 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1582 1583 else: 1584 rawOrders = [] 1585 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1586 1587 return rawOrders 1588 1589 def RequestStopOrders(self) -> list: 1590 """ 1591 Requesting current actual stop orders for current `accountId`. 1592 1593 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1594 1595 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1596 1597 :return: list of dictionaries with stop orders. 1598 """ 1599 if self.accountId is None or not self.accountId: 1600 uLogger.error("Variable `accountId` must be defined for using this method!") 1601 raise Exception("Account ID required") 1602 1603 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1604 1605 self.body = str({"accountId": self.accountId}) 1606 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1607 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1608 1609 if "stopOrders" in rawResponse.keys(): 1610 rawStopOrders = rawResponse["stopOrders"] 1611 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1612 1613 else: 1614 rawStopOrders = [] 1615 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1616 1617 return rawStopOrders 1618 1619 def Overview(self, show: bool = False, details: str = "full") -> dict: 1620 """ 1621 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1622 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1623 and `overviewBondsCalendarFile` are defined then also save information to file. 1624 1625 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1626 many requests about the state of the portfolio, and then, based on the received data, a large number 1627 of calculation and statistics are collected. 1628 1629 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1630 :param details: how detailed should the information be? 1631 - `full` — shows full available information about portfolio status (by default), 1632 - `positions` — shows only open positions, 1633 - `orders` — shows only sections of open limits and stop orders. 1634 - `digest` — show a short digest of the portfolio status, 1635 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1636 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1637 :return: dictionary with client's raw portfolio and some statistics. 1638 """ 1639 if self.accountId is None or not self.accountId: 1640 uLogger.error("Variable `accountId` must be defined for using this method!") 1641 raise Exception("Account ID required") 1642 1643 view = { 1644 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1645 "headers": {}, # list of dictionaries, response headers without "positions" section 1646 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1647 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1648 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1649 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1650 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1651 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1652 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1653 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1654 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1655 }, 1656 "stat": { # --- some statistics calculated using "raw" sections: 1657 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1658 "availableRUB": 0., # available rubles (without other currencies) 1659 "blockedRUB": 0., # blocked sum in Russian Rouble 1660 "totalChangesRUB": 0., # changes for all open trades in RUB 1661 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1662 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1663 "sharesCostRUB": 0., # costs of all shares in RUB 1664 "bondsCostRUB": 0., # costs of all bonds in RUB 1665 "etfsCostRUB": 0., # costs of all etfs in RUB 1666 "futuresCostRUB": 0., # costs of all futures in RUB 1667 "Currencies": [], # list of dictionaries of all currencies statistics 1668 "Shares": [], # list of dictionaries of all shares statistics 1669 "Bonds": [], # list of dictionaries of all bonds statistics 1670 "Etfs": [], # list of dictionaries of all etfs statistics 1671 "Futures": [], # list of dictionaries of all futures statistics 1672 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1673 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1674 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1675 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1676 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1677 }, 1678 "analytics": { # --- some analytics of portfolio: 1679 "distrByAssets": {}, # portfolio distribution by assets 1680 "distrByCompanies": {}, # portfolio distribution by companies 1681 "distrBySectors": {}, # portfolio distribution by sectors 1682 "distrByCurrencies": {}, # portfolio distribution by currencies 1683 "distrByCountries": {}, # portfolio distribution by countries 1684 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1685 } 1686 } 1687 1688 details = details.lower() 1689 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1690 if details not in availableDetails: 1691 details = "full" 1692 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1693 1694 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1695 1696 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1697 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1698 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1699 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1700 1701 # save response headers without "positions" section: 1702 for key in portfolioResponse.keys(): 1703 if key != "positions": 1704 view["raw"]["headers"][key] = portfolioResponse[key] 1705 1706 else: 1707 continue 1708 1709 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1710 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1711 for item in portfolioResponse["positions"]: 1712 if item["instrumentType"] == "currency": 1713 self._figi = item["figi"] 1714 if not self._figi and item["ticker"]: 1715 self._ticker = item["ticker"] 1716 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1717 1718 curr = self.SearchByFIGI(requestPrice=False) 1719 1720 # current price of currency in RUB: 1721 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1722 "name": curr["name"], 1723 "currentPrice": NanoToFloat( 1724 item["currentPrice"]["units"], 1725 item["currentPrice"]["nano"] 1726 ), 1727 } 1728 1729 view["raw"]["Currencies"].append(item) 1730 1731 elif item["instrumentType"] == "share": 1732 view["raw"]["Shares"].append(item) 1733 1734 elif item["instrumentType"] == "bond": 1735 view["raw"]["Bonds"].append(item) 1736 1737 elif item["instrumentType"] == "etf": 1738 view["raw"]["Etfs"].append(item) 1739 1740 elif item["instrumentType"] == "futures": 1741 view["raw"]["Futures"].append(item) 1742 1743 else: 1744 continue 1745 1746 # how many volume of currencies (by ISO currency name) are blocked: 1747 for item in view["raw"]["positions"]["blocked"]: 1748 blocked = NanoToFloat(item["units"], item["nano"]) 1749 if blocked > 0: 1750 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1751 1752 # how many volume of instruments (by FIGI) are blocked: 1753 for item in view["raw"]["positions"]["securities"]: 1754 blocked = int(item["blocked"]) 1755 if blocked > 0: 1756 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1757 1758 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1759 1760 if "rub" in allBlocked.keys(): 1761 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1762 1763 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1764 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1765 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1766 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1767 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1768 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1769 view["stat"]["portfolioCostRUB"] = sum([ 1770 view["stat"]["allCurrenciesCostRUB"], 1771 view["stat"]["sharesCostRUB"], 1772 view["stat"]["bondsCostRUB"], 1773 view["stat"]["etfsCostRUB"], 1774 view["stat"]["futuresCostRUB"], 1775 ]) 1776 1777 # --- calculating some portfolio statistics: 1778 byComp = {} # distribution by companies 1779 bySect = {} # distribution by sectors 1780 byCurr = {} # distribution by currencies (include RUB) 1781 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1782 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1783 1784 for item in portfolioResponse["positions"]: 1785 self._figi = item["figi"] 1786 if not self._figi and item["ticker"]: 1787 self._ticker = item["ticker"] 1788 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1789 1790 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1791 1792 if instrument: 1793 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1794 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1795 1796 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1797 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1798 1799 else: 1800 blocked = 0 1801 1802 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1803 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1804 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1805 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1806 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1807 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1808 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1809 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1810 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1811 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1812 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1813 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1814 1815 statData = { 1816 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1817 "ticker": instrument["ticker"], # ticker by FIGI 1818 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1819 "volume": volume, # available volume of instrument 1820 "lots": lots, # volume in lots of instrument 1821 "direction": direction, # direction of an instrument's position: short or long 1822 "blocked": blocked, # blocked volume of currency or instrument 1823 "currentPrice": curPrice, # current instrument's price in basic asset 1824 "average": average, # current average position price 1825 "cost": cost, # current cost of all volume of instrument in basic asset 1826 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1827 "costRUB": costRUB, # cost of instrument in ruble 1828 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1829 "profit": profit, # expected profit at current moment 1830 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1831 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1832 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1833 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1834 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1835 "step": instrument["step"], # minimum price increment 1836 } 1837 1838 # adding distribution by unique countries: 1839 if statData["country"] not in byCountry.keys(): 1840 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1841 1842 else: 1843 byCountry[statData["country"]]["cost"] += costRUB 1844 byCountry[statData["country"]]["percent"] += percentCostRUB 1845 1846 if item["instrumentType"] != "currency": 1847 # adding distribution by unique companies: 1848 if statData["name"]: 1849 if statData["name"] not in byComp.keys(): 1850 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1851 1852 else: 1853 byComp[statData["name"]]["cost"] += costRUB 1854 byComp[statData["name"]]["percent"] += percentCostRUB 1855 1856 # adding distribution by unique sectors: 1857 if statData["sector"] not in bySect.keys(): 1858 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1859 1860 else: 1861 bySect[statData["sector"]]["cost"] += costRUB 1862 bySect[statData["sector"]]["percent"] += percentCostRUB 1863 1864 # adding distribution by unique currencies: 1865 if currency not in byCurr.keys(): 1866 byCurr[currency] = { 1867 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1868 "cost": costRUB, 1869 "percent": percentCostRUB 1870 } 1871 1872 else: 1873 byCurr[currency]["cost"] += costRUB 1874 byCurr[currency]["percent"] += percentCostRUB 1875 1876 # saving statistics for every instrument: 1877 if item["instrumentType"] == "currency": 1878 view["stat"]["Currencies"].append(statData) 1879 1880 # update dict with free funds for trading (total - blocked) by currencies 1881 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1882 view["stat"]["funds"][currency] = { 1883 "total": volume, 1884 "totalCostRUB": costRUB, # total volume cost in rubles 1885 "free": volume - blocked, 1886 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1887 } 1888 1889 elif item["instrumentType"] == "share": 1890 view["stat"]["Shares"].append(statData) 1891 1892 elif item["instrumentType"] == "bond": 1893 view["stat"]["Bonds"].append(statData) 1894 1895 elif item["instrumentType"] == "etf": 1896 view["stat"]["Etfs"].append(statData) 1897 1898 elif item["instrumentType"] == "Futures": 1899 view["stat"]["Futures"].append(statData) 1900 1901 else: 1902 continue 1903 1904 # total changes in Russian Ruble: 1905 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1906 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1907 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1908 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1909 view["stat"]["funds"]["rub"] = { 1910 "total": view["stat"]["availableRUB"], 1911 "totalCostRUB": view["stat"]["availableRUB"], 1912 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1913 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1914 } 1915 1916 # --- pending limit orders sector data: 1917 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1918 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1919 1920 for item in view["raw"]["orders"]: 1921 self._figi = item["figi"] 1922 1923 if item["figi"] not in uniquePendingOrdersFIGIs: 1924 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1925 1926 uniquePendingOrdersFIGIs.append(item["figi"]) 1927 uniquePendingOrders[item["figi"]] = instrument 1928 1929 else: 1930 instrument = uniquePendingOrders[item["figi"]] 1931 1932 if instrument: 1933 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1934 orderType = TKS_ORDER_TYPES[item["orderType"]] 1935 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1936 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1937 1938 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1939 if item["direction"] == "ORDER_DIRECTION_BUY": 1940 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1941 1942 else: 1943 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1944 1945 # requested price for order execution: 1946 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1947 1948 # necessary changes in percent to reach target from current price: 1949 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1950 1951 view["stat"]["orders"].append({ 1952 "orderID": item["orderId"], # orderId number parameter of current order 1953 "figi": item["figi"], # FIGI identification 1954 "ticker": instrument["ticker"], # ticker name by FIGI 1955 "lotsRequested": item["lotsRequested"], # requested lots value 1956 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1957 "currentPrice": lastPrice, # current instrument's price for defined action 1958 "targetPrice": target, # requested price for order execution in base currency 1959 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1960 "percentChanges": changes, # changes in percent to target from current price 1961 "currency": item["currency"], # instrument's currency name 1962 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1963 "type": orderType, # type of order from TKS_ORDER_TYPES 1964 "status": orderState, # order status from TKS_ORDER_STATES 1965 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1966 }) 1967 1968 # --- stop orders sector data: 1969 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1970 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1971 1972 for item in view["raw"]["stopOrders"]: 1973 self._figi = item["figi"] 1974 1975 if item["figi"] not in uniqueStopOrdersFIGIs: 1976 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1977 1978 uniqueStopOrdersFIGIs.append(item["figi"]) 1979 uniqueStopOrders[item["figi"]] = instrument 1980 1981 else: 1982 instrument = uniqueStopOrders[item["figi"]] 1983 1984 if instrument: 1985 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1986 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1987 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1988 1989 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1990 if "expirationTime" in item.keys(): 1991 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1992 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1993 1994 else: 1995 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1996 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1997 1998 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1999 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2000 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2001 2002 else: 2003 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2004 2005 # requested price when stop-order executed: 2006 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2007 2008 # price for limit-order, set up when stop-order executed: 2009 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2010 2011 # necessary changes in percent to reach target from current price: 2012 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2013 2014 view["stat"]["stopOrders"].append({ 2015 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2016 "figi": item["figi"], # FIGI identification 2017 "ticker": instrument["ticker"], # ticker name by FIGI 2018 "lotsRequested": item["lotsRequested"], # requested lots value 2019 "currentPrice": lastPrice, # current instrument's price for defined action 2020 "targetPrice": target, # requested price for stop-order execution in base currency 2021 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2022 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2023 "percentChanges": changes, # changes in percent to target from current price 2024 "currency": item["currency"], # instrument's currency name 2025 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2026 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2027 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2028 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2029 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2030 }) 2031 2032 # --- calculating data for analytics section: 2033 # portfolio distribution by assets: 2034 view["analytics"]["distrByAssets"] = { 2035 "Ruble": { 2036 "uniques": 1, 2037 "cost": view["stat"]["availableRUB"], 2038 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2039 }, 2040 "Currencies": { 2041 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2042 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2043 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2044 }, 2045 "Shares": { 2046 "uniques": len(view["stat"]["Shares"]), 2047 "cost": view["stat"]["sharesCostRUB"], 2048 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2049 }, 2050 "Bonds": { 2051 "uniques": len(view["stat"]["Bonds"]), 2052 "cost": view["stat"]["bondsCostRUB"], 2053 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2054 }, 2055 "Etfs": { 2056 "uniques": len(view["stat"]["Etfs"]), 2057 "cost": view["stat"]["etfsCostRUB"], 2058 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2059 }, 2060 "Futures": { 2061 "uniques": len(view["stat"]["Futures"]), 2062 "cost": view["stat"]["futuresCostRUB"], 2063 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2064 }, 2065 } 2066 2067 # portfolio distribution by companies: 2068 view["analytics"]["distrByCompanies"]["All money cash"] = { 2069 "ticker": "", 2070 "cost": view["stat"]["allCurrenciesCostRUB"], 2071 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2072 } 2073 view["analytics"]["distrByCompanies"].update(byComp) 2074 2075 # portfolio distribution by sectors: 2076 view["analytics"]["distrBySectors"]["All money cash"] = { 2077 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2078 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2079 } 2080 view["analytics"]["distrBySectors"].update(bySect) 2081 2082 # portfolio distribution by currencies: 2083 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2084 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2085 2086 if self.moreDebug: 2087 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2088 2089 view["analytics"]["distrByCurrencies"].update(byCurr) 2090 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2091 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2092 2093 # portfolio distribution by countries: 2094 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2096 2097 if self.moreDebug: 2098 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2099 2100 view["analytics"]["distrByCountries"].update(byCountry) 2101 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2102 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2103 2104 # --- Prepare text statistics overview in human-readable: 2105 if show: 2106 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2107 2108 # Whatever the value `details`, header not changes: 2109 info = [ 2110 "# Client's portfolio\n\n", 2111 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2112 "* **Account ID:** [{}]\n".format(self.accountId), 2113 ] 2114 2115 if details in ["full", "positions", "digest"]: 2116 info.extend([ 2117 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2118 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2119 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2120 view["stat"]["totalChangesRUB"], 2121 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2122 view["stat"]["totalChangesPercentRUB"], 2123 ), 2124 ]) 2125 2126 if details in ["full", "positions"]: 2127 info.extend([ 2128 "## Open positions\n\n", 2129 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2130 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2131 "| **Ruble:** | {:>31} | | | | | |\n".format( 2132 "{:.2f} ({:.2f}) rub".format( 2133 view["stat"]["availableRUB"], 2134 view["stat"]["blockedRUB"], 2135 ) 2136 ) 2137 ]) 2138 2139 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2140 return [ 2141 "| | | | | | | |\n", 2142 "| {:<27} | | | | | {:>19} | |\n".format( 2143 noTradeStr if noTradeStr else typeStr, 2144 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2145 ), 2146 ] 2147 2148 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2149 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2150 "{} [{}]".format(data["ticker"], data["figi"]), 2151 "{:.2f} ({:.2f}) {}".format( 2152 data["volume"], 2153 data["blocked"], 2154 data["currency"], 2155 ) if isCurr else "{:.0f} ({:.0f})".format( 2156 data["volume"], 2157 data["blocked"], 2158 ), 2159 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2160 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2161 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2162 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2163 "{}{:.2f} {} ({}{:.2f}%)".format( 2164 "+" if data["profit"] > 0 else "", 2165 data["profit"], data["baseCurrencyName"], 2166 "+" if data["percentProfit"] > 0 else "", 2167 data["percentProfit"], 2168 ), 2169 ) 2170 2171 # --- Show currencies section: 2172 if view["stat"]["Currencies"]: 2173 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2174 for item in view["stat"]["Currencies"]: 2175 info.append(_InfoStr(item, isCurr=True)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2179 2180 # --- Show shares section: 2181 if view["stat"]["Shares"]: 2182 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2183 2184 for item in view["stat"]["Shares"]: 2185 info.append(_InfoStr(item)) 2186 2187 else: 2188 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2189 2190 # --- Show bonds section: 2191 if view["stat"]["Bonds"]: 2192 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2193 2194 for item in view["stat"]["Bonds"]: 2195 info.append(_InfoStr(item)) 2196 2197 else: 2198 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2199 2200 # --- Show etfs section: 2201 if view["stat"]["Etfs"]: 2202 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2203 2204 for item in view["stat"]["Etfs"]: 2205 info.append(_InfoStr(item)) 2206 2207 else: 2208 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2209 2210 # --- Show futures section: 2211 if view["stat"]["Futures"]: 2212 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2213 2214 for item in view["stat"]["Futures"]: 2215 info.append(_InfoStr(item)) 2216 2217 else: 2218 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2219 2220 if details in ["full", "orders"]: 2221 # --- Show pending limit orders section: 2222 if view["stat"]["orders"]: 2223 info.extend([ 2224 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2225 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2226 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2227 ]) 2228 2229 for item in view["stat"]["orders"]: 2230 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2231 "{} [{}]".format(item["ticker"], item["figi"]), 2232 item["orderID"], 2233 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2234 "{} {} ({}{:.2f}%)".format( 2235 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2236 item["baseCurrencyName"], 2237 "+" if item["percentChanges"] > 0 else "", 2238 float(item["percentChanges"]), 2239 ), 2240 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2241 item["action"], 2242 item["type"], 2243 item["date"], 2244 )) 2245 2246 else: 2247 info.append("\n## Total pending limit-orders: [0]\n") 2248 2249 # --- Show stop orders section: 2250 if view["stat"]["stopOrders"]: 2251 info.extend([ 2252 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2253 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2254 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2255 ]) 2256 2257 for item in view["stat"]["stopOrders"]: 2258 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2259 "{} [{}]".format(item["ticker"], item["figi"]), 2260 item["orderID"], 2261 item["lotsRequested"], 2262 "{} {} ({}{:.2f}%)".format( 2263 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2264 item["baseCurrencyName"], 2265 "+" if item["percentChanges"] > 0 else "", 2266 float(item["percentChanges"]), 2267 ), 2268 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2269 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2270 item["action"], 2271 item["type"], 2272 item["expType"], 2273 item["createDate"], 2274 item["expDate"], 2275 )) 2276 2277 else: 2278 info.append("\n## Total stop-orders: [0]\n") 2279 2280 if details in ["full", "analytics"]: 2281 # -- Show analytics section: 2282 if view["stat"]["portfolioCostRUB"] > 0: 2283 info.extend([ 2284 "\n# Analytics\n\n" 2285 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2286 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2287 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2288 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2289 view["stat"]["totalChangesRUB"], 2290 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2291 view["stat"]["totalChangesPercentRUB"], 2292 ), 2293 "\n## Portfolio distribution by assets\n" 2294 "\n| Type | Uniques | Percent | Current cost |\n", 2295 "|------------------------------------|---------|---------|--------------------|\n", 2296 ]) 2297 2298 for key in view["analytics"]["distrByAssets"].keys(): 2299 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2300 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2301 key, 2302 view["analytics"]["distrByAssets"][key]["uniques"], 2303 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2304 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2305 )) 2306 2307 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2308 2309 info.extend([ 2310 "\n## Portfolio distribution by companies\n" 2311 "\n| Company | Percent | Current cost |\n", 2312 aSepLine, 2313 ]) 2314 2315 for company in view["analytics"]["distrByCompanies"].keys(): 2316 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2317 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2318 "{}{}".format( 2319 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2320 company, 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2324 )) 2325 2326 info.extend([ 2327 "\n## Portfolio distribution by sectors\n" 2328 "\n| Sector | Percent | Current cost |\n", 2329 aSepLine, 2330 ]) 2331 2332 for sector in view["analytics"]["distrBySectors"].keys(): 2333 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2334 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2335 sector, 2336 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2338 )) 2339 2340 info.extend([ 2341 "\n## Portfolio distribution by currencies\n" 2342 "\n| Instruments currencies | Percent | Current cost |\n", 2343 aSepLine, 2344 ]) 2345 2346 for curr in view["analytics"]["distrByCurrencies"].keys(): 2347 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2348 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2349 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2350 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2351 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2352 )) 2353 2354 info.extend([ 2355 "\n## Portfolio distribution by countries\n" 2356 "\n| Assets by country | Percent | Current cost |\n", 2357 aSepLine, 2358 ]) 2359 2360 for country in view["analytics"]["distrByCountries"].keys(): 2361 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2362 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2363 country, 2364 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2365 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2366 )) 2367 2368 if details in ["full", "calendar"]: 2369 # -- Show bonds payment calendar section: 2370 if view["stat"]["Bonds"]: 2371 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2372 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2373 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2374 2375 else: 2376 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2377 2378 infoText = "".join(info) 2379 2380 uLogger.info(infoText) 2381 2382 if details == "full" and self.overviewFile: 2383 filename = self.overviewFile 2384 2385 elif details == "digest" and self.overviewDigestFile: 2386 filename = self.overviewDigestFile 2387 2388 elif details == "positions" and self.overviewPositionsFile: 2389 filename = self.overviewPositionsFile 2390 2391 elif details == "orders" and self.overviewOrdersFile: 2392 filename = self.overviewOrdersFile 2393 2394 elif details == "analytics" and self.overviewAnalyticsFile: 2395 filename = self.overviewAnalyticsFile 2396 2397 elif details == "calendar" and self.overviewBondsCalendarFile: 2398 filename = self.overviewBondsCalendarFile 2399 2400 else: 2401 filename = "" 2402 2403 if filename: 2404 with open(filename, "w", encoding="UTF-8") as fH: 2405 fH.write(infoText) 2406 2407 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2408 2409 if self.useHTMLReports: 2410 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2411 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2412 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2413 2414 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2415 2416 return view 2417 2418 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2419 """ 2420 Returns history operations between two given dates for current `accountId`. 2421 If `reportFile` string is not empty then also save human-readable report. 2422 Shows some statistical data of closed positions. 2423 2424 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2425 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2426 :param show: if `True` then also prints all records to the console. 2427 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2428 :return: original list of dictionaries with history of deals records from API ("operations" key): 2429 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2430 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2431 """ 2432 if self.accountId is None or not self.accountId: 2433 uLogger.error("Variable `accountId` must be defined for using this method!") 2434 raise Exception("Account ID required") 2435 2436 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2437 2438 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2439 2440 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2441 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2442 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2443 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2444 customStat = {} # custom statistics in additional to responseJSON 2445 2446 # --- output report in human-readable format: 2447 if show or self.reportFile: 2448 splitLine1 = "| | | | | |\n" # Summary section 2449 splitLine2 = "| | | | | | | | |\n" # Operations section 2450 nextDay = "" 2451 2452 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2453 2454 if len(ops) > 0: 2455 customStat = { 2456 "opsCount": 0, # total operations count 2457 "buyCount": 0, # buy operations 2458 "sellCount": 0, # sell operations 2459 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2460 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2461 "payIn": {"rub": 0.}, # Deposit brokerage account 2462 "payOut": {"rub": 0.}, # Withdrawals 2463 "divs": {"rub": 0.}, # Dividends income 2464 "coupons": {"rub": 0.}, # Coupon's income 2465 "brokerCom": {"rub": 0.}, # Service commissions 2466 "serviceCom": {"rub": 0.}, # Service commissions 2467 "marginCom": {"rub": 0.}, # Margin commissions 2468 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2469 } 2470 2471 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2472 for item in ops: 2473 if item["state"] == "OPERATION_STATE_EXECUTED": 2474 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2475 2476 # count buy operations: 2477 if "_BUY" in item["operationType"]: 2478 customStat["buyCount"] += 1 2479 2480 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2481 customStat["buyTotal"][item["payment"]["currency"]] += payment 2482 2483 else: 2484 customStat["buyTotal"][item["payment"]["currency"]] = payment 2485 2486 # count sell operations: 2487 elif "_SELL" in item["operationType"]: 2488 customStat["sellCount"] += 1 2489 2490 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2491 customStat["sellTotal"][item["payment"]["currency"]] += payment 2492 2493 else: 2494 customStat["sellTotal"][item["payment"]["currency"]] = payment 2495 2496 # count incoming operations: 2497 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2498 if item["payment"]["currency"] in customStat["payIn"].keys(): 2499 customStat["payIn"][item["payment"]["currency"]] += payment 2500 2501 else: 2502 customStat["payIn"][item["payment"]["currency"]] = payment 2503 2504 # count withdrawals operations: 2505 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2506 if item["payment"]["currency"] in customStat["payOut"].keys(): 2507 customStat["payOut"][item["payment"]["currency"]] += payment 2508 2509 else: 2510 customStat["payOut"][item["payment"]["currency"]] = payment 2511 2512 # count dividends income: 2513 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2514 if item["payment"]["currency"] in customStat["divs"].keys(): 2515 customStat["divs"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["divs"][item["payment"]["currency"]] = payment 2519 2520 # count coupon's income: 2521 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2522 if item["payment"]["currency"] in customStat["coupons"].keys(): 2523 customStat["coupons"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["coupons"][item["payment"]["currency"]] = payment 2527 2528 # count broker commissions: 2529 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2530 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2531 customStat["brokerCom"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["brokerCom"][item["payment"]["currency"]] = payment 2535 2536 # count service commissions: 2537 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2538 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2539 customStat["serviceCom"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["serviceCom"][item["payment"]["currency"]] = payment 2543 2544 # count margin commissions: 2545 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2546 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2547 customStat["marginCom"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["marginCom"][item["payment"]["currency"]] = payment 2551 2552 # count withholding taxes: 2553 elif "_TAX" in item["operationType"]: 2554 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2555 customStat["allTaxes"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["allTaxes"][item["payment"]["currency"]] = payment 2559 2560 else: 2561 continue 2562 2563 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2564 2565 # --- view "Actions" lines: 2566 info.extend([ 2567 "| Report sections | | | | |\n", 2568 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2569 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2570 "| | Buy: {:<22} | {:<28} | | |\n".format( 2571 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2572 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2573 ), 2574 "| | Sell: {:<21} | {:<28} | | |\n".format( 2575 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2576 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2577 ), 2578 ]) 2579 2580 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2581 for key in opsKeys: 2582 if key == "rub": 2583 continue 2584 2585 info.extend([ 2586 "| | | {:<28} | | |\n".format( 2587 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2588 ), 2589 "| | | {:<28} | | |\n".format( 2590 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2591 ), 2592 ]) 2593 2594 info.append(splitLine1) 2595 2596 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2597 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2598 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2601 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2602 ) 2603 2604 # --- view "Payments" lines: 2605 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2606 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2607 2608 for key in paymentsKeys: 2609 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2610 2611 info.append(splitLine1) 2612 2613 # --- view "Commissions and taxes" lines: 2614 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2615 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2616 2617 for key in comKeys: 2618 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2619 2620 info.extend([ 2621 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2622 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2623 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2624 ]) 2625 2626 else: 2627 info.append("Broker returned no operations during this period\n") 2628 2629 # --- view "Operations" section: 2630 for item in ops: 2631 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2632 continue 2633 2634 else: 2635 self._figi = item["figi"] 2636 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2637 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2638 2639 # group of deals during one day: 2640 if nextDay and item["date"].split("T")[0] != nextDay: 2641 info.append(splitLine2) 2642 nextDay = "" 2643 2644 else: 2645 nextDay = item["date"].split("T")[0] # saving current day for splitting 2646 2647 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2648 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2649 self._figi if self._figi else "—", 2650 instrument["ticker"] if instrument else "—", 2651 instrument["type"] if instrument else "—", 2652 item["quantity"] if int(item["quantity"]) > 0 else "—", 2653 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2654 TKS_OPERATION_STATES[item["state"]], 2655 TKS_OPERATION_TYPES[item["operationType"]], 2656 )) 2657 2658 infoText = "".join(info) 2659 2660 if show: 2661 if self.moreDebug: 2662 uLogger.debug("Records about history of a client's operations successfully received") 2663 2664 uLogger.info(infoText) 2665 2666 if self.reportFile: 2667 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2668 fH.write(infoText) 2669 2670 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2671 2672 if self.useHTMLReports: 2673 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2674 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2675 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2676 2677 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2678 2679 return ops, customStat 2680 2681 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2682 """ 2683 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2684 2685 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2686 Warning! Broker server used ISO UTC time by default. 2687 2688 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2689 Also, `historyFile` used to update history with `onlyMissing` parameter. 2690 2691 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2692 2693 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2695 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2696 `"hour"`, `"day"`. Default: `"hour"`. 2697 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2698 False by default. Warning! History appends only from last candle to current time 2699 with always update last candle! 2700 :param csvSep: separator if csv-file is used, `,` by default. 2701 :param show: if `True` then also prints Pandas DataFrame to the console. 2702 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2703 `["date", "time", "open", "high", "low", "close", "volume"]`. 2704 """ 2705 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2706 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2707 history = None # empty pandas object for history 2708 2709 if interval not in TKS_CANDLE_INTERVALS.keys(): 2710 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2711 raise Exception("Incorrect value") 2712 2713 if not (self._ticker or self._figi): 2714 uLogger.error("Ticker or FIGI must be defined!") 2715 raise Exception("Ticker or FIGI required") 2716 2717 if self._ticker and not self._figi: 2718 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2719 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2720 2721 if self._figi and not self._ticker: 2722 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2723 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2724 2725 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2726 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2727 if interval.lower() != "day": 2728 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2729 2730 delta = dtEnd - dtStart # current UTC time minus last time in file 2731 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2732 2733 # calculate history length in candles: 2734 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2735 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2736 length += 1 # to avoid fraction time 2737 2738 # calculate data blocks count: 2739 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2740 2741 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2742 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2743 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2744 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2745 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2746 2747 tempOld = None # pandas object for old history, if --only-missing key present 2748 lastTime = None # datetime object of last old candle in file 2749 2750 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2751 uLogger.debug("--only-missing key present, add only last missing candles...") 2752 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2753 2754 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2755 2756 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2757 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2758 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2759 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2760 2761 # get last datetime object from last string in file or minus 1 delta if file is empty: 2762 if len(tempOld) > 0: 2763 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2764 2765 else: 2766 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2767 2768 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2769 2770 responseJSONs = [] # raw history blocks of data 2771 2772 blockEnd = dtEnd 2773 for item in range(blocks): 2774 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2775 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2776 2777 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2778 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2779 )) 2780 2781 if blockStart == blockEnd: 2782 uLogger.debug("Skipped this zero-length block...") 2783 2784 else: 2785 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2786 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2787 self.body = str({ 2788 "figi": self._figi, 2789 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2790 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2791 "interval": TKS_CANDLE_INTERVALS[interval][0] 2792 }) 2793 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2794 2795 if "code" in responseJSON.keys(): 2796 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2797 2798 else: 2799 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2800 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2801 2802 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2803 2804 blockEnd = blockStart 2805 2806 printCount = len(responseJSONs) # candles to show in console 2807 if responseJSONs: 2808 tempHistory = pd.DataFrame( 2809 data={ 2810 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2811 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2812 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2813 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2814 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2815 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2816 "volume": [int(item["volume"]) for item in responseJSONs], 2817 }, 2818 index=range(len(responseJSONs)), 2819 columns=["date", "time", "open", "high", "low", "close", "volume"], 2820 ) 2821 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2822 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2823 2824 # append only newest candles to old history if --only-missing key present: 2825 if onlyMissing and tempOld is not None and lastTime is not None: 2826 index = 0 # find start index in tempHistory data: 2827 2828 for i, item in tempHistory.iterrows(): 2829 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2830 2831 if curTime == lastTime: 2832 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2833 index = i 2834 printCount = index + 1 2835 break 2836 2837 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2838 2839 else: 2840 history = tempHistory # if no `--only-missing` key then load full data from server 2841 2842 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2843 2844 if history is not None and not history.empty: 2845 if show: 2846 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2847 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2848 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2849 )) 2850 2851 else: 2852 uLogger.warning("Received an empty candles history!") 2853 2854 if self.historyFile is not None: 2855 if history is not None and not history.empty: 2856 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2857 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2858 2859 else: 2860 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2861 2862 else: 2863 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2864 2865 return history 2866 2867 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2868 """ 2869 Load candles history from csv-file and return Pandas DataFrame object. 2870 2871 See also: `History()` and `ShowHistoryChart()` methods. 2872 2873 :param filePath: path to csv-file to open. 2874 """ 2875 loadedHistory = None # init candles data object 2876 2877 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2878 2879 if os.path.exists(filePath): 2880 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2881 2882 tfStr = self.priceModel.FormattedDelta( 2883 self.priceModel.timeframe, 2884 "{days} days {hours}h {minutes}m {seconds}s", 2885 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2886 self.priceModel.timeframe, 2887 "{hours}h {minutes}m {seconds}s", 2888 ) 2889 2890 if loadedHistory is not None and not loadedHistory.empty: 2891 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2892 len(loadedHistory), 2893 tfStr, 2894 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2895 ) 2896 2897 else: 2898 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2899 2900 else: 2901 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2902 2903 return loadedHistory 2904 2905 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2906 """ 2907 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2908 2909 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2910 Default: `index.html` (both for interact and non-interact candlesticks chart). 2911 2912 See also: `History()` and `LoadHistory()` methods. 2913 2914 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2915 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2916 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2917 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2918 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2919 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2920 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2921 """ 2922 if isinstance(candles, str): 2923 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2924 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2925 2926 elif isinstance(candles, pd.DataFrame): 2927 self.priceModel.prices = candles # set candles chain from variable 2928 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2929 2930 if "datetime" not in candles.columns: 2931 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2932 2933 else: 2934 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2935 raise Exception("Incorrect value") 2936 2937 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2938 2939 if interact: 2940 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2941 2942 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2943 2944 else: 2945 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2946 2947 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2948 2949 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2950 2951 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2952 """ 2953 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2954 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2955 2956 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2957 2958 :param operation: string "Buy" or "Sell". 2959 :param lots: volume, integer count of lots >= 1. 2960 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2961 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2962 :param expDate: string "Undefined" by default or local date in future, 2963 it is a string with format `%Y-%m-%d %H:%M:%S`. 2964 :return: JSON with response from broker server. 2965 """ 2966 if self.accountId is None or not self.accountId: 2967 uLogger.error("Variable `accountId` must be defined for using this method!") 2968 raise Exception("Account ID required") 2969 2970 if operation is None or not operation or operation not in ("Buy", "Sell"): 2971 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2972 raise Exception("Incorrect value") 2973 2974 if lots is None or lots < 1: 2975 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2976 lots = 1 2977 2978 if tp is None or tp < 0: 2979 tp = 0 2980 2981 if sl is None or sl < 0: 2982 sl = 0 2983 2984 if expDate is None or not expDate: 2985 expDate = "Undefined" 2986 2987 if not (self._ticker or self._figi): 2988 uLogger.error("Ticker or FIGI must be defined!") 2989 raise Exception("Ticker or FIGI required") 2990 2991 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2992 self._ticker = instrument["ticker"] 2993 self._figi = instrument["figi"] 2994 2995 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2996 2997 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2998 self.body = str({ 2999 "figi": self._figi, 3000 "quantity": str(lots), 3001 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3002 "accountId": str(self.accountId), 3003 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3004 }) 3005 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3006 3007 if "orderId" in response.keys(): 3008 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3009 operation, response["orderId"], 3010 self._ticker, self._figi, lots, 3011 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3012 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3013 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3014 )) 3015 3016 if tp > 0: 3017 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3018 3019 if sl > 0: 3020 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3021 3022 else: 3023 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3024 3025 return response 3026 3027 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3028 """ 3029 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3030 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3031 3032 See also: `Order()` and `Trade()` docstrings. 3033 3034 :param lots: volume, integer count of lots >= 1. 3035 :param tp: float > 0, take profit price of stop-order. 3036 :param sl: float > 0, stop loss price of stop-order. 3037 :param expDate: it's a local date in future. 3038 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3039 :return: JSON with response from broker server. 3040 """ 3041 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3042 3043 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3044 """ 3045 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3046 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3047 3048 See also: `Order()` and `Trade()` docstrings. 3049 3050 :param lots: volume, integer count of lots >= 1. 3051 :param tp: float > 0, take profit price of stop-order. 3052 :param sl: float > 0, stop loss price of stop-order. 3053 :param expDate: it's a local date in the future. 3054 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3055 :return: JSON with response from broker server. 3056 """ 3057 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3058 3059 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3060 """ 3061 Close position of given instruments. 3062 3063 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3064 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3065 This avoids unnecessary downloading data from the server. 3066 """ 3067 if instruments is None or not instruments: 3068 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3069 raise Exception("Ticker or FIGI required") 3070 3071 if isinstance(instruments, str): 3072 instruments = [instruments] 3073 3074 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3075 if uniqueInstruments: 3076 if portfolio is None or not portfolio: 3077 portfolio = self.Overview(show=False) 3078 3079 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3080 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3081 3082 for self._figi in uniqueInstruments: 3083 if self._figi not in allOpened: 3084 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3085 continue 3086 3087 # search open trade info about instrument by ticker: 3088 instrument = {} 3089 for iType in TKS_INSTRUMENTS: 3090 if instrument: 3091 break 3092 3093 for item in portfolio["stat"][iType]: 3094 if item["figi"] == self._figi: 3095 instrument = item 3096 break 3097 3098 if instrument: 3099 self._ticker = instrument["ticker"] 3100 self._figi = instrument["figi"] 3101 3102 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3103 self._ticker, 3104 self._figi, 3105 int(instrument["volume"]), 3106 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3107 )) 3108 3109 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3110 3111 if tradeLots > 0: 3112 if instrument["blocked"] > 0: 3113 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3114 instrument["blocked"], 3115 self._ticker, 3116 tradeLots, 3117 )) 3118 3119 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3120 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3121 3122 else: 3123 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3124 3125 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3126 """ 3127 Close all positions of given instruments with defined type. 3128 3129 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3130 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3131 This avoids unnecessary downloading data from the server. 3132 """ 3133 if iType not in TKS_INSTRUMENTS: 3134 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3135 3136 else: 3137 if portfolio is None or not portfolio: 3138 portfolio = self.Overview(show=False) 3139 3140 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3141 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3142 3143 if tickers and portfolio: 3144 self.CloseTrades(tickers, portfolio) 3145 3146 else: 3147 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3148 3149 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3150 """ 3151 Universal method to create market or limit orders with all available parameters for current `accountId`. 3152 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3153 3154 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3155 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3156 3157 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3158 then broker immediately open market order as you can do simple --buy or --sell operations! 3159 3160 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3161 When current price will go up or down to target price value then broker opens a limit order. 3162 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3163 3164 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3165 3166 :param operation: string "Buy" or "Sell". 3167 :param orderType: string "Limit" or "Stop". 3168 :param lots: volume, integer count of lots >= 1. 3169 :param targetPrice: target price > 0. This is open trade price for limit order. 3170 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3171 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3172 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3173 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3174 Stop loss order always executed by market price. 3175 :param expDate: string "Undefined" by default or local date in future. 3176 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3177 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3178 A limit order has no expiration date, it lasts until the end of the trading day. 3179 :return: JSON with response from broker server. 3180 """ 3181 if self.accountId is None or not self.accountId: 3182 uLogger.error("Variable `accountId` must be defined for using this method!") 3183 raise Exception("Account ID required") 3184 3185 if operation is None or not operation or operation not in ("Buy", "Sell"): 3186 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3187 raise Exception("Incorrect value") 3188 3189 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3190 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3191 raise Exception("Incorrect value") 3192 3193 if lots is None or lots < 1: 3194 uLogger.error("You must define trade volume > 0: integer count of lots!") 3195 raise Exception("Incorrect value") 3196 3197 if targetPrice is None or targetPrice <= 0: 3198 uLogger.error("Target price for limit-order must be greater than 0!") 3199 raise Exception("Incorrect value") 3200 3201 if limitPrice is None or limitPrice <= 0: 3202 limitPrice = targetPrice 3203 3204 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3205 stopType = "Limit" 3206 3207 if expDate is None or not expDate: 3208 expDate = "Undefined" 3209 3210 if not (self._ticker or self._figi): 3211 uLogger.error("Tocker or FIGI must be defined!") 3212 raise Exception("Ticker or FIGI required") 3213 3214 response = {} 3215 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3216 self._ticker = instrument["ticker"] 3217 self._figi = instrument["figi"] 3218 3219 if orderType == "Limit": 3220 uLogger.debug( 3221 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3222 self._ticker, self._figi, 3223 operation, lots, targetPrice, instrument["currency"], 3224 )) 3225 3226 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3227 self.body = str({ 3228 "figi": self._figi, 3229 "quantity": str(lots), 3230 "price": FloatToNano(targetPrice), 3231 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3232 "accountId": str(self.accountId), 3233 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3234 }) 3235 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3236 3237 if "orderId" in response.keys(): 3238 uLogger.info( 3239 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3240 response["orderId"], self._ticker, self._figi, operation, lots, 3241 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3242 )) 3243 3244 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3245 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3246 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3247 targetPrice, instrument["currency"], 3248 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3249 )) 3250 3251 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3252 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3253 targetPrice, instrument["currency"], 3254 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3255 )) 3256 3257 else: 3258 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3259 3260 if orderType == "Stop": 3261 uLogger.debug( 3262 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3263 self._ticker, self._figi, 3264 operation, lots, 3265 targetPrice, instrument["currency"], 3266 limitPrice, instrument["currency"], 3267 stopType, expDate, 3268 )) 3269 3270 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3271 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3272 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3273 3274 body = { 3275 "figi": self._figi, 3276 "quantity": str(lots), 3277 "price": FloatToNano(limitPrice), 3278 "stopPrice": FloatToNano(targetPrice), 3279 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3280 "accountId": str(self.accountId), 3281 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3282 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3283 } 3284 3285 if expDateUTC: 3286 body["expireDate"] = expDateUTC 3287 3288 self.body = str(body) 3289 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3290 3291 if "stopOrderId" in response.keys(): 3292 uLogger.info( 3293 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3294 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3295 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3296 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3297 TKS_STOP_ORDER_TYPES[stopOrderType], 3298 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3299 )) 3300 3301 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3302 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3303 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3304 targetPrice, instrument["currency"], 3305 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3306 )) 3307 3308 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3309 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3310 targetPrice, instrument["currency"], 3311 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3312 )) 3313 3314 else: 3315 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3316 3317 return response 3318 3319 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3320 """ 3321 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3322 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3323 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3324 See also: `Order()` docstring. 3325 3326 :param lots: volume, integer count of lots >= 1. 3327 :param targetPrice: target price > 0. This is open trade price for limit order. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3331 3332 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3333 """ 3334 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3335 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3336 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3337 target price value then broker opens a limit order. See also: `Order()` docstring. 3338 3339 :param lots: volume, integer count of lots >= 1. 3340 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3341 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3342 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3343 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3344 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3345 :param expDate: string "Undefined" by default or local date in future. 3346 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3347 This date is converting to UTC format for server. 3348 :return: JSON with response from broker server. 3349 """ 3350 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3351 3352 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3353 """ 3354 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3355 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3356 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3357 See also: `Order()` docstring. 3358 3359 :param lots: volume, integer count of lots >= 1. 3360 :param targetPrice: target price > 0. This is open trade price for limit order. 3361 :return: JSON with response from broker server. 3362 """ 3363 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3364 3365 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3366 """ 3367 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3368 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3369 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3370 target price value then broker opens a limit order. See also: `Order()` docstring. 3371 3372 :param lots: volume, integer count of lots >= 1. 3373 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3374 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3375 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3376 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3377 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3378 :param expDate: string "Undefined" by default or local date in future. 3379 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3380 This date is converting to UTC format for server. 3381 :return: JSON with response from broker server. 3382 """ 3383 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3384 3385 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3386 """ 3387 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3388 3389 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3390 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3391 This avoids unnecessary downloading data from the server. 3392 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3393 """ 3394 if self.accountId is None or not self.accountId: 3395 uLogger.error("Variable `accountId` must be defined for using this method!") 3396 raise Exception("Account ID required") 3397 3398 if orderIDs: 3399 if allOrdersIDs is None: 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3402 3403 if allStopOrdersIDs is None: 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 3407 for orderID in orderIDs: 3408 idInPendingOrders = orderID in allOrdersIDs 3409 idInStopOrders = orderID in allStopOrdersIDs 3410 3411 if not (idInPendingOrders or idInStopOrders): 3412 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3413 continue 3414 3415 else: 3416 if idInPendingOrders: 3417 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3418 3419 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3420 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3421 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3422 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3423 3424 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3425 if self.moreDebug: 3426 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3427 3428 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3429 3430 else: 3431 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3432 3433 elif idInStopOrders: 3434 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3435 3436 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3437 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3438 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3439 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3440 3441 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3442 if self.moreDebug: 3443 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3444 3445 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3446 3447 else: 3448 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3449 3450 else: 3451 continue 3452 3453 def CloseAllOrders(self) -> None: 3454 """ 3455 Gets a list of open pending and stop orders and cancel it all. 3456 """ 3457 rawOrders = self.RequestPendingOrders() 3458 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3459 lenOrders = len(allOrdersIDs) 3460 3461 rawStopOrders = self.RequestStopOrders() 3462 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3463 lenSOrders = len(allStopOrdersIDs) 3464 3465 if lenOrders > 0 or lenSOrders > 0: 3466 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3467 3468 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3469 3470 else: 3471 uLogger.info("Orders not found, nothing to cancel.") 3472 3473 def CloseAll(self, *args) -> None: 3474 """ 3475 Close all available (not blocked) opened trades and orders. 3476 3477 Also, you can select one or more keywords case-insensitive: 3478 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3479 3480 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3481 """ 3482 overview = self.Overview(show=False) # get all open trades info 3483 3484 if len(args) == 0: 3485 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3486 self.CloseAllOrders() # close all pending and stop orders 3487 3488 for iType in TKS_INSTRUMENTS: 3489 if iType != "Currencies": 3490 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3491 3492 else: 3493 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3494 lowerArgs = [x.lower() for x in args] 3495 3496 if "orders" in lowerArgs: 3497 self.CloseAllOrders() # close all pending and stop orders 3498 3499 for iType in TKS_INSTRUMENTS: 3500 if iType.lower() in lowerArgs and iType != "Currencies": 3501 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3502 3503 def CloseAllByTicker(self, instrument: str) -> None: 3504 """ 3505 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3506 3507 This method searches opened trade and orders of instrument throw all portfolio and then use 3508 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3509 3510 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3511 3512 :param instrument: string with ticker. 3513 """ 3514 if instrument is None or not instrument: 3515 uLogger.error("Ticker name must be defined for using this method!") 3516 raise Exception("Ticker required") 3517 3518 overview = self.Overview(show=False) # get user portfolio with all open trades info 3519 3520 self._ticker = instrument # try to set instrument as ticker 3521 self._figi = "" 3522 3523 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3524 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3525 3526 if limitAll and self.IsInLimitOrders(portfolio=overview): 3527 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3528 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3529 3530 if stopAll and self.IsInStopOrders(portfolio=overview): 3531 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3532 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3533 3534 if self.IsInPortfolio(portfolio=overview): 3535 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3536 self.CloseTrades(instruments=[instrument], portfolio=overview) 3537 3538 def CloseAllByFIGI(self, instrument: str) -> None: 3539 """ 3540 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3541 3542 This method searches opened trade and orders of instrument throw all portfolio and then use 3543 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3544 3545 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3546 3547 :param instrument: string with FIGI id. 3548 """ 3549 if instrument is None or not instrument: 3550 uLogger.error("FIGI id must be defined for using this method!") 3551 raise Exception("FIGI required") 3552 3553 overview = self.Overview(show=False) # get user portfolio with all open trades info 3554 3555 self._ticker = "" 3556 self._figi = instrument # try to set instrument as FIGI id 3557 3558 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3559 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3560 3561 if limitAll and self.IsInLimitOrders(portfolio=overview): 3562 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3563 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3564 3565 if stopAll and self.IsInStopOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if self.IsInPortfolio(portfolio=overview): 3570 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3571 self.CloseTrades(instruments=[instrument], portfolio=overview) 3572 3573 @staticmethod 3574 def ParseOrderParameters(operation, **inputParameters): 3575 """ 3576 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3577 3578 :param operation: string "Buy" or "Sell". 3579 :param inputParameters: this is dict of strings that looks like this 3580 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3581 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3582 "prices" key: one or more prices to open limit-orders 3583 Counts of values in lots and prices lists must be equals! 3584 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3585 """ 3586 # TODO: update order grid work with api v2 3587 pass 3588 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3589 # 3590 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3591 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3592 # raise Exception("Incorrect value") 3593 # 3594 # if "l" in inputParameters.keys(): 3595 # inputParameters["lots"] = inputParameters.pop("l") 3596 # 3597 # if "p" in inputParameters.keys(): 3598 # inputParameters["prices"] = inputParameters.pop("p") 3599 # 3600 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3601 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3602 # raise Exception("Incorrect value") 3603 # 3604 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3605 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3606 # 3607 # if len(lots) != len(prices): 3608 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3609 # raise Exception("Incorrect value") 3610 # 3611 # uLogger.debug("Extracted parameters for orders:") 3612 # uLogger.debug("lots = {}".format(lots)) 3613 # uLogger.debug("prices = {}".format(prices)) 3614 # 3615 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3616 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3617 # uLogger.debug("Order parameters: {}".format(result)) 3618 # 3619 # return result 3620 3621 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3622 """ 3623 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3624 3625 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3626 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3627 """ 3628 result = False 3629 msg = "Instrument not defined!" 3630 3631 if portfolio is None or not portfolio: 3632 portfolio = self.Overview(show=False) 3633 3634 if self._ticker: 3635 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3636 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3637 3638 for iType in TKS_INSTRUMENTS: 3639 for instrument in portfolio["stat"][iType]: 3640 if instrument["ticker"] == self._ticker: 3641 result = True 3642 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3643 break 3644 3645 elif self._figi: 3646 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3647 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3648 3649 for iType in TKS_INSTRUMENTS: 3650 for instrument in portfolio["stat"][iType]: 3651 if instrument["figi"] == self._figi: 3652 result = True 3653 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3654 break 3655 3656 else: 3657 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3658 3659 uLogger.debug(msg) 3660 3661 return result 3662 3663 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3664 """ 3665 Returns instrument from the user's portfolio if it presents there. 3666 Instrument must be defined by `ticker` (highly priority) or `figi`. 3667 3668 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3669 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3670 """ 3671 result = None 3672 msg = "Instrument not defined!" 3673 3674 if portfolio is None or not portfolio: 3675 portfolio = self.Overview(show=False) 3676 3677 if self._ticker: 3678 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3679 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3680 3681 for iType in TKS_INSTRUMENTS: 3682 for instrument in portfolio["stat"][iType]: 3683 if instrument["ticker"] == self._ticker: 3684 result = instrument 3685 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3686 break 3687 3688 elif self._figi: 3689 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3690 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3691 3692 for iType in TKS_INSTRUMENTS: 3693 for instrument in portfolio["stat"][iType]: 3694 if instrument["figi"] == self._figi: 3695 result = instrument 3696 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3697 break 3698 3699 else: 3700 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3701 3702 uLogger.debug(msg) 3703 3704 return result 3705 3706 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3707 """ 3708 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3709 3710 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3711 3712 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3713 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3714 """ 3715 result = False 3716 msg = "Instrument not defined!" 3717 3718 if portfolio is None or not portfolio: 3719 portfolio = self.Overview(show=False) 3720 3721 if self._ticker: 3722 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3723 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3724 3725 for instrument in portfolio["stat"]["orders"]: 3726 if instrument["ticker"] == self._ticker: 3727 result = True 3728 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3729 break 3730 3731 elif self._figi: 3732 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3733 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3734 3735 for instrument in portfolio["stat"]["orders"]: 3736 if instrument["figi"] == self._figi: 3737 result = True 3738 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3739 break 3740 3741 else: 3742 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3743 3744 uLogger.debug(msg) 3745 3746 return result 3747 3748 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3749 """ 3750 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3751 Instrument must be defined by `ticker` (highly priority) or `figi`. 3752 3753 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3754 3755 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3756 :return: list with `orderID`s of limit orders. 3757 """ 3758 result = [] 3759 msg = "Instrument not defined!" 3760 3761 if portfolio is None or not portfolio: 3762 portfolio = self.Overview(show=False) 3763 3764 if self._ticker: 3765 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3766 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3767 3768 for instrument in portfolio["stat"]["orders"]: 3769 if instrument["ticker"] == self._ticker: 3770 result.append(instrument["orderID"]) 3771 3772 if result: 3773 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3774 3775 elif self._figi: 3776 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3777 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3778 3779 for instrument in portfolio["stat"]["orders"]: 3780 if instrument["figi"] == self._figi: 3781 result.append(instrument["orderID"]) 3782 3783 if result: 3784 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3785 3786 else: 3787 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3788 3789 uLogger.debug(msg) 3790 3791 return result 3792 3793 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3794 """ 3795 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3796 3797 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3798 3799 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3800 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3801 """ 3802 result = False 3803 msg = "Instrument not defined!" 3804 3805 if portfolio is None or not portfolio: 3806 portfolio = self.Overview(show=False) 3807 3808 if self._ticker: 3809 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3810 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3811 3812 for instrument in portfolio["stat"]["stopOrders"]: 3813 if instrument["ticker"] == self._ticker: 3814 result = True 3815 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3816 break 3817 3818 elif self._figi: 3819 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3820 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3821 3822 for instrument in portfolio["stat"]["stopOrders"]: 3823 if instrument["figi"] == self._figi: 3824 result = True 3825 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3826 break 3827 3828 else: 3829 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3830 3831 uLogger.debug(msg) 3832 3833 return result 3834 3835 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3836 """ 3837 Returns list with all `orderID`s of opened stop orders for the instrument. 3838 Instrument must be defined by `ticker` (highly priority) or `figi`. 3839 3840 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3841 3842 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3843 :return: list with `orderID`s of stop orders. 3844 """ 3845 result = [] 3846 msg = "Instrument not defined!" 3847 3848 if portfolio is None or not portfolio: 3849 portfolio = self.Overview(show=False) 3850 3851 if self._ticker: 3852 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3853 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3854 3855 for instrument in portfolio["stat"]["stopOrders"]: 3856 if instrument["ticker"] == self._ticker: 3857 result.append(instrument["orderID"]) 3858 3859 if result: 3860 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3861 3862 elif self._figi: 3863 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3864 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3865 3866 for instrument in portfolio["stat"]["stopOrders"]: 3867 if instrument["figi"] == self._figi: 3868 result.append(instrument["orderID"]) 3869 3870 if result: 3871 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3872 3873 else: 3874 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3875 3876 uLogger.debug(msg) 3877 3878 return result 3879 3880 def RequestLimits(self) -> dict: 3881 """ 3882 Method for obtaining the available funds for withdrawal for current `accountId`. 3883 3884 See also: 3885 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3886 - `OverviewLimits()` method 3887 3888 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3889 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3890 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3891 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3892 """ 3893 if self.accountId is None or not self.accountId: 3894 uLogger.error("Variable `accountId` must be defined for using this method!") 3895 raise Exception("Account ID required") 3896 3897 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3898 3899 self.body = str({"accountId": self.accountId}) 3900 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3901 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3902 3903 if self.moreDebug: 3904 uLogger.debug("Records about available funds for withdrawal successfully received") 3905 3906 return rawLimits 3907 3908 def OverviewLimits(self, show: bool = False) -> dict: 3909 """ 3910 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3911 3912 See also: `RequestLimits()`. 3913 3914 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3915 :return: dict with raw parsed data from server and some calculated statistics about it. 3916 """ 3917 if self.accountId is None or not self.accountId: 3918 uLogger.error("Variable `accountId` must be defined for using this method!") 3919 raise Exception("Account ID required") 3920 3921 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3922 3923 view = { 3924 "rawLimits": rawLimits, 3925 "limits": { # parsed data for every currency: 3926 "money": { # this is an array of portfolio currency positions 3927 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3928 }, 3929 "blocked": { # this is an array of blocked currency 3930 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3931 }, 3932 "blockedGuarantee": { # this is locked money under collateral for futures 3933 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3934 }, 3935 }, 3936 } 3937 3938 # --- Prepare text table with limits in human-readable format: 3939 if show: 3940 info = [ 3941 "# Withdrawal limits\n\n", 3942 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3943 "* **Account ID:** [{}]\n".format(self.accountId), 3944 ] 3945 3946 if view["limits"]["money"]: 3947 info.extend([ 3948 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3949 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3950 ]) 3951 3952 else: 3953 info.append("\nNo withdrawal limits\n") 3954 3955 for curr in view["limits"]["money"].keys(): 3956 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3957 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3958 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3959 3960 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3961 "[{}]".format(curr), 3962 "{:.2f}".format(view["limits"]["money"][curr]), 3963 "{:.2f}".format(availableMoney), 3964 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3965 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3966 ) 3967 3968 if curr == "rub": 3969 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3970 3971 else: 3972 info.append(infoStr) 3973 3974 infoText = "".join(info) 3975 3976 uLogger.info(infoText) 3977 3978 if self.withdrawalLimitsFile: 3979 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3980 fH.write(infoText) 3981 3982 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3983 3984 if self.useHTMLReports: 3985 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3986 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3987 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3988 3989 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3990 3991 return view 3992 3993 def RequestAccounts(self) -> dict: 3994 """ 3995 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3996 3997 See also: 3998 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3999 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4000 - `OverviewUserInfo()` method 4001 4002 :return: dict with raw data from server that contains accounts info. Example of dict: 4003 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4004 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4005 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4006 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4007 """ 4008 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4009 4010 self.body = str({}) 4011 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4012 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4013 4014 if self.moreDebug: 4015 uLogger.debug("Records about available accounts successfully received") 4016 4017 return rawAccounts 4018 4019 def RequestUserInfo(self) -> dict: 4020 """ 4021 Method for requesting common user's information. 4022 4023 See also: 4024 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4025 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4026 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4027 - `OverviewUserInfo()` method 4028 4029 :return: dict with raw data from server that contains user's information. Example of dict: 4030 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4031 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4032 """ 4033 uLogger.debug("Requesting common user's information. Wait, please...") 4034 4035 self.body = str({}) 4036 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4037 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4038 4039 if self.moreDebug: 4040 uLogger.debug("Records about current user successfully received") 4041 4042 return rawUserInfo 4043 4044 def RequestMarginStatus(self, accountId: str = None) -> dict: 4045 """ 4046 Method for requesting margin calculation for defined account ID. 4047 4048 See also: 4049 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4050 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4051 - `OverviewUserInfo()` method 4052 4053 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4054 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4055 Example of responses: 4056 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4057 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4058 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4059 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4060 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4061 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4062 """ 4063 if accountId is None or not accountId: 4064 if self.accountId is None or not self.accountId: 4065 uLogger.error("Variable `accountId` must be defined for using this method!") 4066 raise Exception("Account ID required") 4067 4068 else: 4069 accountId = self.accountId # use `self.accountId` (main ID) by default 4070 4071 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4072 4073 self.body = str({"accountId": accountId}) 4074 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4075 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4076 4077 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4078 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4079 rawMargin = {} 4080 4081 else: 4082 if self.moreDebug: 4083 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4084 4085 return rawMargin 4086 4087 def RequestTariffLimits(self) -> dict: 4088 """ 4089 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4090 4091 See also: 4092 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4093 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4094 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4095 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4096 - `OverviewUserInfo()` method 4097 4098 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4099 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4100 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4101 """ 4102 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4103 4104 self.body = str({}) 4105 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4106 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4107 4108 if self.moreDebug: 4109 uLogger.debug("Records with limits of current tariff successfully received") 4110 4111 return rawTariffLimits 4112 4113 def RequestBondCoupons(self, iJSON: dict) -> dict: 4114 """ 4115 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4116 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4117 All dates are in UTC timezone. 4118 4119 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4120 Documentation: 4121 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4122 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4123 4124 See also: `ExtendBondsData()`. 4125 4126 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4127 If raw iJSON is not data of bond then server returns an error [400] with message: 4128 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4129 :return: dictionary with bond payment calendar. Response example 4130 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4131 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4132 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4133 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4134 """ 4135 if iJSON["figi"] is None or not iJSON["figi"]: 4136 uLogger.error("FIGI must be defined for using this method!") 4137 raise Exception("FIGI required") 4138 4139 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4140 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4141 4142 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4143 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4144 self._figi, 4145 startDate, 4146 endDate, 4147 )) 4148 4149 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4150 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4151 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4152 4153 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4154 uLogger.warning("Instrument type is not bond!") 4155 4156 else: 4157 if self.moreDebug: 4158 uLogger.debug("Records about bond payment calendar successfully received") 4159 4160 return calendar 4161 4162 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4163 """ 4164 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4165 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4166 coupon yields, current yields and some statistics etc. 4167 4168 WARNING! This is too long operation if a lot of bonds requested from broker server. 4169 4170 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4171 4172 :param instruments: list of strings with tickers or FIGIs. 4173 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4174 for further used by data scientists or stock analytics. 4175 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4176 In XLSX-file and Pandas DataFrame fields mean: 4177 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4178 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4179 """ 4180 if instruments is None or not instruments: 4181 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4182 raise Exception("Ticker or FIGI required") 4183 4184 if isinstance(instruments, str): 4185 instruments = [instruments] 4186 4187 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4188 4189 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4190 4191 iCount = len(uniqueInstruments) 4192 tooLong = iCount >= 20 4193 if tooLong: 4194 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4195 4196 bonds = None 4197 for i, self._figi in enumerate(uniqueInstruments): 4198 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4199 4200 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4201 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4202 rawBond = self.SearchByFIGI(requestPrice=True) 4203 4204 # Widen raw data with UTC current time (iData["actualDateTime"]): 4205 actualDate = datetime.now(tzutc()) 4206 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4207 4208 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4209 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4210 4211 # Replace some values with human-readable: 4212 iData["nominalCurrency"] = iData["nominal"]["currency"] 4213 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4214 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4215 iData["aciCurrency"] = iData["aciValue"]["currency"] 4216 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4217 iData["issueSize"] = int(iData["issueSize"]) 4218 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4219 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4220 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4221 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4222 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4223 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4224 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4225 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4226 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4227 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4228 4229 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4230 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4231 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4232 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4233 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4234 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4235 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4236 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4237 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4238 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4239 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4240 4241 # Widen raw data with calendar data from `rawCalendar` values: 4242 calendarData = [] 4243 if "events" in iData["rawCalendar"].keys(): 4244 for item in iData["rawCalendar"]["events"]: 4245 calendarData.append({ 4246 "couponDate": item["couponDate"], 4247 "couponNumber": int(item["couponNumber"]), 4248 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4249 "payCurrency": item["payOneBond"]["currency"], 4250 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4251 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4252 "couponStartDate": item["couponStartDate"], 4253 "couponEndDate": item["couponEndDate"], 4254 "couponPeriod": item["couponPeriod"], 4255 }) 4256 4257 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4258 if "maturityDate" not in iData.keys(): 4259 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4260 4261 # Widen raw data with Coupon Rate. 4262 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4263 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4264 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4265 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4266 4267 # Widen raw data with Yield to Maturity (YTM) on current date. 4268 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4269 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4270 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4271 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4272 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4273 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4274 4275 iData["calendar"] = calendarData # adds calendar at the end 4276 4277 # Remove not used data: 4278 iData.pop("uid") 4279 iData.pop("positionUid") 4280 iData.pop("currentPrice") 4281 iData.pop("rawCalendar") 4282 4283 colNames = list(iData.keys()) 4284 if bonds is None: 4285 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4286 4287 else: 4288 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4289 4290 else: 4291 uLogger.warning("Instrument is not a bond!") 4292 4293 processed = round(100 * (i + 1) / iCount, 1) 4294 if tooLong and processed % 5 == 0: 4295 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4296 4297 else: 4298 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4299 4300 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4301 4302 # Saving bonds from Pandas DataFrame to XLSX sheet: 4303 if xlsx and self.bondsXLSXFile: 4304 with pd.ExcelWriter( 4305 path=self.bondsXLSXFile, 4306 date_format=TKS_DATE_FORMAT, 4307 datetime_format=TKS_DATE_TIME_FORMAT, 4308 mode="w", 4309 ) as writer: 4310 bonds.to_excel( 4311 writer, 4312 sheet_name="Extended bonds data", 4313 index=True, 4314 encoding="UTF-8", 4315 freeze_panes=(1, 1), 4316 ) # saving as XLSX-file with freeze first row and column as headers 4317 4318 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4319 4320 return bonds 4321 4322 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4323 """ 4324 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4325 4326 WARNING! This is too long operation if a lot of bonds requested from broker server. 4327 4328 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4329 4330 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4331 extended information about bonds: main info, current prices, bond payment calendar, 4332 coupon yields, current yields and some statistics etc. 4333 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4334 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4335 for further used by data scientists or stock analytics. 4336 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4337 """ 4338 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4339 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4340 4341 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4342 4343 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4344 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4345 calendar = None 4346 for bond in extBonds.iterrows(): 4347 for item in bond[1]["calendar"]: 4348 cData = { 4349 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4350 "couponDate": item["couponDate"], 4351 "figi": bond[1]["figi"], 4352 "ticker": bond[1]["ticker"], 4353 "name": bond[1]["name"], 4354 "couponNumber": item["couponNumber"], 4355 "payOneBond": item["payOneBond"], 4356 "payCurrency": item["payCurrency"], 4357 "couponType": item["couponType"], 4358 "couponPeriod": item["couponPeriod"], 4359 "fixDate": item["fixDate"], 4360 "couponStartDate": item["couponStartDate"], 4361 "couponEndDate": item["couponEndDate"], 4362 } 4363 4364 if calendar is None: 4365 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4366 4367 else: 4368 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4369 4370 if calendar is not None: 4371 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4372 4373 # Saving calendar from Pandas DataFrame to XLSX sheet: 4374 if xlsx: 4375 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4376 4377 with pd.ExcelWriter( 4378 path=xlsxCalendarFile, 4379 date_format=TKS_DATE_FORMAT, 4380 datetime_format=TKS_DATE_TIME_FORMAT, 4381 mode="w", 4382 ) as writer: 4383 humanReadable = calendar.copy(deep=True) 4384 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4387 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4388 humanReadable.columns = colNames # human-readable column names 4389 4390 humanReadable.to_excel( 4391 writer, 4392 sheet_name="Bond payments calendar", 4393 index=False, 4394 encoding="UTF-8", 4395 freeze_panes=(1, 2), 4396 ) # saving as XLSX-file with freeze first row and column as headers 4397 4398 del humanReadable # release df in memory 4399 4400 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4401 4402 return calendar 4403 4404 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4405 """ 4406 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4407 Also, creates Markdown file with calendar data, `calendar.md` by default. 4408 4409 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4410 4411 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4412 extended information about bonds: main info, current prices, bond payment calendar, 4413 coupon yields, current yields and some statistics etc. 4414 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4415 :param show: if `True` then also printing bonds payment calendar to the console, 4416 otherwise save to file `calendarFile` only. `False` by default. 4417 :return: multilines text in Markdown format with bonds payment calendar as a table. 4418 """ 4419 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4420 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4421 4422 infoText = "# Bond payments calendar\n\n" 4423 4424 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4425 4426 if not (calendar is None or calendar.empty): 4427 splitLine = "| | | | | | | | | |\n" 4428 4429 info = [ 4430 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4431 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4432 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4433 ] 4434 4435 newMonth = False 4436 notOneBond = calendar["figi"].nunique() > 1 4437 for i, bond in enumerate(calendar.iterrows()): 4438 if newMonth and notOneBond: 4439 info.append(splitLine) 4440 4441 info.append( 4442 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4443 " √" if bond[1]["paid"] else " —", 4444 bond[1]["couponDate"].split("T")[0], 4445 bond[1]["figi"], 4446 bond[1]["ticker"], 4447 bond[1]["couponNumber"], 4448 "{} {}".format( 4449 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4450 bond[1]["payCurrency"], 4451 ), 4452 bond[1]["couponType"], 4453 bond[1]["couponPeriod"], 4454 bond[1]["fixDate"].split("T")[0], 4455 ) 4456 ) 4457 4458 if i < len(calendar.values) - 1: 4459 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4460 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4461 newMonth = False if curDate.month == nextDate.month else True 4462 4463 else: 4464 newMonth = False 4465 4466 infoText += "".join(info) 4467 4468 if show: 4469 uLogger.info("{}".format(infoText)) 4470 4471 if self.calendarFile is not None: 4472 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4473 fH.write(infoText) 4474 4475 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4476 4477 if self.useHTMLReports: 4478 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4479 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4480 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4481 4482 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4483 4484 else: 4485 infoText += "No data\n" 4486 4487 return infoText 4488 4489 def OverviewAccounts(self, show: bool = False) -> dict: 4490 """ 4491 Method for parsing and show simple table with all available user accounts. 4492 4493 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4494 4495 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4496 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4497 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4498 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4499 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4500 "closed": "—", "access": "Full access" }, ...}}` 4501 """ 4502 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4503 4504 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4505 accounts = { 4506 item["id"]: { 4507 "type": TKS_ACCOUNT_TYPES[item["type"]], 4508 "name": item["name"], 4509 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4510 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4511 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4512 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4513 } for item in rawAccounts["accounts"] 4514 } 4515 4516 # Raw and parsed data with some fields replaced in "stat" section: 4517 view = { 4518 "rawAccounts": rawAccounts, 4519 "stat": accounts, 4520 } 4521 4522 # --- Prepare simple text table with only accounts data in human-readable format: 4523 if show: 4524 info = [ 4525 "# User accounts\n\n", 4526 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4527 "| Account ID | Type | Status | Name |\n", 4528 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4529 ] 4530 4531 for account in view["stat"].keys(): 4532 info.extend([ 4533 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4534 account, 4535 view["stat"][account]["type"], 4536 view["stat"][account]["status"], 4537 view["stat"][account]["name"], 4538 ) 4539 ]) 4540 4541 infoText = "".join(info) 4542 4543 uLogger.info(infoText) 4544 4545 if self.userAccountsFile: 4546 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4547 fH.write(infoText) 4548 4549 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4550 4551 if self.useHTMLReports: 4552 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4553 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4554 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4555 4556 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4557 4558 return view 4559 4560 def OverviewUserInfo(self, show: bool = False) -> dict: 4561 """ 4562 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4563 4564 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4565 4566 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4567 :return: dict with raw parsed data from server and some calculated statistics about it. 4568 """ 4569 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4570 tmpTicker = self._ticker 4571 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4572 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4573 self._ticker = tmpTicker 4574 4575 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4576 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4577 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4578 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4579 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4580 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4581 4582 # This is dict with parsed common user data: 4583 userInfo = { 4584 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4585 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4586 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4587 "tariff": rawUserInfo["tariff"], 4588 } 4589 4590 # This is an array of dict with parsed margin statuses for every account IDs: 4591 margins = {} 4592 for accountId in accounts.keys(): 4593 if rawMargins[accountId]: 4594 margins[accountId] = { 4595 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4596 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4597 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4598 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4599 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4600 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4601 "missing": missing["volume"], 4602 } 4603 4604 else: 4605 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4606 4607 unary = {} # unary-connection limits 4608 for item in rawTariffLimits["unaryLimits"]: 4609 if item["limitPerMinute"] in unary.keys(): 4610 unary[item["limitPerMinute"]].extend(item["methods"]) 4611 4612 else: 4613 unary[item["limitPerMinute"]] = item["methods"] 4614 4615 stream = {} # stream-connection limits 4616 for item in rawTariffLimits["streamLimits"]: 4617 if item["limit"] in stream.keys(): 4618 stream[item["limit"]].extend(item["streams"]) 4619 4620 else: 4621 stream[item["limit"]] = item["streams"] 4622 4623 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4624 limits = { 4625 "unary": unary, 4626 "stream": stream, 4627 } 4628 4629 # Raw and parsed data as an output result: 4630 view = { 4631 "rawUserInfo": rawUserInfo, 4632 "rawAccounts": rawAccounts, 4633 "rawMargins": rawMargins, 4634 "rawTariffLimits": rawTariffLimits, 4635 "stat": { 4636 "overview": overview, 4637 "userInfo": userInfo, 4638 "accounts": accounts, 4639 "margins": margins, 4640 "limits": limits, 4641 }, 4642 } 4643 4644 # --- Prepare text table with user information in human-readable format: 4645 if show: 4646 info = [ 4647 "# Full user information\n\n", 4648 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4649 "## Common information\n\n", 4650 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4651 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4652 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4653 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4654 "\n## User accounts\n\n", 4655 ] 4656 4657 for account in view["stat"]["accounts"].keys(): 4658 info.extend([ 4659 "### ID: [{}]\n\n".format(account), 4660 "| Parameters | Values |\n", 4661 "|----------------------|--------------------------------------------------------------|\n", 4662 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4663 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4664 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4665 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4666 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4667 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4668 ]) 4669 4670 if margins[account]: 4671 info.extend([ 4672 "| Margin status: | Enabled |\n", 4673 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4674 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4675 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4676 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4677 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4678 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4679 ]) 4680 4681 else: 4682 info.append("| Margin status: | Disabled |\n\n") 4683 4684 info.extend([ 4685 "\n## Current user tariff limits\n", 4686 "\n### See also\n", 4687 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4688 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4689 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4690 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4691 "\n### Unary limits\n", 4692 ]) 4693 4694 if unary: 4695 for key, values in sorted(unary.items()): 4696 info.append("\n* Max requests per minute: {}\n".format(key)) 4697 4698 for value in values: 4699 info.append(" - {}\n".format(value)) 4700 4701 else: 4702 info.append("\nNot available\n") 4703 4704 info.append("\n### Stream limits\n") 4705 4706 if stream: 4707 for key, values in sorted(stream.items()): 4708 info.append("\n* Max stream connections: {}\n".format(key)) 4709 4710 for value in values: 4711 info.append(" - {}\n".format(value)) 4712 4713 else: 4714 info.append("\nNot available\n") 4715 4716 infoText = "".join(info) 4717 4718 uLogger.info(infoText) 4719 4720 if self.userInfoFile: 4721 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4722 fH.write(infoText) 4723 4724 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4725 4726 if self.useHTMLReports: 4727 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4728 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4729 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4730 4731 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4732 4733 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
419 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 420 """ 421 Send GET or POST request to broker server and receive JSON object. 422 423 self.header: must be defining with dictionary of headers. 424 self.body: if define then used as request body. None by default. 425 self.timeout: global request timeout, 15 seconds by default. 426 :param url: url with REST request. 427 :param reqType: send "GET" or "POST" request. "GET" by default. 428 :param retry: how many times retry after first request if an 5xx server errors occurred. 429 :param pause: sleep time in seconds between retries. 430 :return: response JSON (dictionary) from broker. 431 """ 432 if reqType.upper() not in ("GET", "POST"): 433 uLogger.error("You can define request type: `GET` or `POST`!") 434 raise Exception("Incorrect value") 435 436 if self.moreDebug: 437 uLogger.debug("Request parameters:") 438 uLogger.debug(" - REST API URL: {}".format(url)) 439 uLogger.debug(" - request type: {}".format(reqType)) 440 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 441 uLogger.debug(" - body:\n{}".format(self.body)) 442 443 # fast hack to avoid all operations with some tickers/FIGI 444 responseJSON = {} 445 oK = True 446 for item in self.exclude: 447 if item in url: 448 if self.moreDebug: 449 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 450 451 oK = False 452 break 453 454 if oK: 455 with self.__lock: # acquire the mutex lock 456 counter = 0 457 response = None 458 errMsg = "" 459 460 while not response and counter <= retry: 461 if reqType == "GET": 462 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 463 464 if reqType == "POST": 465 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 466 467 if self.moreDebug: 468 uLogger.debug("Response:") 469 uLogger.debug(" - status code: {}".format(response.status_code)) 470 uLogger.debug(" - reason: {}".format(response.reason)) 471 uLogger.debug(" - body length: {}".format(len(response.text))) 472 uLogger.debug(" - headers:\n{}".format(response.headers)) 473 474 # Server returns some headers: 475 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 476 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 477 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 478 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 479 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 480 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 481 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 482 sleep(rateLimitWait) 483 484 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 485 if 400 <= response.status_code < 500: 486 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 487 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 488 489 if "code" in response.text and "message" in response.text: 490 msgDict = self._ParseJSON(rawData=response.text) 491 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 492 493 counter = retry + 1 # do not retry for 4xx errors 494 495 if 500 <= response.status_code < 600: 496 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 497 uLogger.debug(" - not oK, {}".format(errMsg)) 498 499 if "code" in response.text and "message" in response.text: 500 errMsgDict = self._ParseJSON(rawData=response.text) 501 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 502 503 counter += 1 504 505 if counter <= retry: 506 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 507 sleep(pause) 508 509 responseJSON = self._ParseJSON(rawData=response.text) 510 511 if errMsg: 512 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 513 uLogger.error(" - not oK, {}".format(errMsg)) 514 515 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
548 def Listing(self) -> dict: 549 """ 550 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 551 552 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 553 """ 554 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 555 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 556 557 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 558 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 559 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 560 561 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 562 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 563 poolUpdater.close() # close the thread pool 564 poolUpdater.join() # wait a moment until all data returns from threads 565 566 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 567 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 568 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 569 570 # calculate minimum price increment (step) for all instruments and set up instrument's type: 571 for iType in iList.keys(): 572 for ticker in iList[iType]: 573 iList[iType][ticker]["type"] = iType 574 575 if "minPriceIncrement" in iList[iType][ticker].keys(): 576 iList[iType][ticker]["step"] = NanoToFloat( 577 iList[iType][ticker]["minPriceIncrement"]["units"], 578 iList[iType][ticker]["minPriceIncrement"]["nano"], 579 ) 580 581 else: 582 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 583 584 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
586 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 587 """ 588 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 589 590 See also: `DumpInstruments()`, `Listing()`. 591 592 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 593 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 594 """ 595 if self.iListDumpFile is None or not self.iListDumpFile: 596 uLogger.error("Output name of dump file must be defined!") 597 raise Exception("Filename required") 598 599 if not self.iList or forceUpdate: 600 self.iList = self.Listing() 601 602 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 603 604 # Save as XLSX with separated sheets for every type of instruments: 605 with pd.ExcelWriter( 606 path=xlsxDumpFile, 607 date_format=TKS_DATE_FORMAT, 608 datetime_format=TKS_DATE_TIME_FORMAT, 609 mode="w", 610 ) as writer: 611 for iType in TKS_INSTRUMENTS: 612 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 613 df = df[sorted(df)] # sorted by column names 614 df = df.applymap( 615 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 616 na_action="ignore", 617 ) # converting numbers from nano-type to float in every cell 618 df.to_excel( 619 writer, 620 sheet_name=iType, 621 encoding="UTF-8", 622 freeze_panes=(1, 1), 623 ) # saving as XLSX-file with freeze first row and column as headers 624 625 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
627 def DumpInstruments(self, forceUpdate: bool = True) -> str: 628 """ 629 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 630 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 631 632 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 636 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 646 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 647 fH.write(jsonDump) 648 649 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 650 651 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
653 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 654 """ 655 Show information about one instrument defined by json data and prints it in Markdown format. 656 657 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 658 659 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 660 :param show: if `True` then also printing information about instrument and its current price. 661 :return: multilines text in Markdown format with information about one instrument. 662 """ 663 splitLine = "| | |\n" 664 infoText = "" 665 666 if iJSON is not None and iJSON and isinstance(iJSON, dict): 667 info = [ 668 "# Main information\n\n", 669 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 670 "| Parameters | Values |\n", 671 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 672 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 673 "| Full name: | {:<54} |\n".format(iJSON["name"]), 674 ] 675 676 if "sector" in iJSON.keys() and iJSON["sector"]: 677 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 678 679 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 680 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 681 682 info.extend([ 683 splitLine, 684 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 685 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 686 ]) 687 688 if "isin" in iJSON.keys() and iJSON["isin"]: 689 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 690 691 if "classCode" in iJSON.keys(): 692 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 693 694 info.extend([ 695 splitLine, 696 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 697 splitLine, 698 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 699 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 700 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 701 ]) 702 703 if iJSON["figi"]: 704 self._figi = iJSON["figi"] 705 iJSON = iJSON | self.RequestTradingStatus() 706 707 info.extend([ 708 splitLine, 709 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 710 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 711 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 712 ]) 713 714 info.append(splitLine) 715 716 if "type" in iJSON.keys() and iJSON["type"]: 717 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 718 719 if "shareType" in iJSON.keys() and iJSON["shareType"]: 720 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 721 722 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 723 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 724 725 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 726 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 727 728 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 729 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 730 731 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 732 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 733 734 if "focusType" in iJSON.keys() and iJSON["focusType"]: 735 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 736 737 if "assetType" in iJSON.keys() and iJSON["assetType"]: 738 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 739 740 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 741 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 742 743 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 744 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 745 746 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 747 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 748 749 if "currency" in iJSON.keys(): 750 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 751 752 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 753 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 754 755 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 756 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 757 758 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 759 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 760 761 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 762 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 763 764 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 765 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 766 767 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 768 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 769 770 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 771 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 772 773 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 774 info.append("| Perpetual bond: | Yes |\n") 775 776 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 777 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 778 779 iExt = None 780 if iJSON["type"] == "Bonds": 781 info.extend([ 782 splitLine, 783 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 784 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 785 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 786 iJSON["nominal"]["currency"], 787 )), 788 ]) 789 790 if "floatingCouponFlag" in iJSON.keys(): 791 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 792 793 if "amortizationFlag" in iJSON.keys(): 794 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 795 796 info.append(splitLine) 797 798 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 799 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 800 801 if iJSON["figi"]: 802 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 803 804 info.extend([ 805 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 806 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 807 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 808 ]) 809 810 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 811 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 812 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 813 iJSON["aciValue"]["currency"] 814 ))) 815 816 if "currentPrice" in iJSON.keys(): 817 info.append(splitLine) 818 819 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 820 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 821 822 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 823 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 824 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 825 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 826 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 827 828 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 829 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 830 831 info.extend([ 832 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 833 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 834 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 835 )), 836 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 837 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 838 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 839 )), 840 "| Changes between last deal price and last close | {:<54} |\n".format( 841 "{:.2f}%{}".format( 842 iJSON["currentPrice"]["changes"], 843 " ({}{:.2f} {})".format( 844 "+" if bondChangesDelta > 0 else "", 845 bondChangesDelta, 846 aciCurrency 847 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 848 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 849 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 850 currency 851 ), 852 ) 853 ), 854 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 855 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 860 )), 861 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 862 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 863 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 864 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 865 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 866 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 867 )), 868 ]) 869 870 if "lot" in iJSON.keys(): 871 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 872 873 if "step" in iJSON.keys() and iJSON["step"] != 0: 874 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 875 876 # Add bond payment calendar: 877 if iJSON["type"] == "Bonds": 878 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 879 info.extend(["\n#", strCalendar]) 880 881 infoText += "".join(info) 882 883 if show: 884 uLogger.info("{}".format(infoText)) 885 886 else: 887 uLogger.debug("{}".format(infoText)) 888 889 if self.infoFile is not None: 890 with open(self.infoFile, "w", encoding="UTF-8") as fH: 891 fH.write(infoText) 892 893 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 894 895 if self.useHTMLReports: 896 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 897 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 898 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 899 900 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 901 902 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
904 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 905 """ 906 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 907 908 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 909 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 910 :return: JSON formatted data with information about instrument. 911 """ 912 tickerJSON = {} 913 if self.moreDebug: 914 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 915 916 if not self._ticker: 917 uLogger.warning("self._ticker variable is not be empty!") 918 919 else: 920 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 921 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 922 raise Exception("Instrument not allowed") 923 924 if not self.iList: 925 self.iList = self.Listing() 926 927 if self._ticker in self.iList["Shares"].keys(): 928 tickerJSON = self.iList["Shares"][self._ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 931 932 elif self._ticker in self.iList["Currencies"].keys(): 933 tickerJSON = self.iList["Currencies"][self._ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 936 937 elif self._ticker in self.iList["Bonds"].keys(): 938 tickerJSON = self.iList["Bonds"][self._ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 941 942 elif self._ticker in self.iList["Etfs"].keys(): 943 tickerJSON = self.iList["Etfs"][self._ticker] 944 if self.moreDebug: 945 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 946 947 elif self._ticker in self.iList["Futures"].keys(): 948 tickerJSON = self.iList["Futures"][self._ticker] 949 if self.moreDebug: 950 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 951 952 if tickerJSON: 953 self._figi = tickerJSON["figi"] 954 955 if requestPrice: 956 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 957 958 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 959 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 960 961 else: 962 tickerJSON["currentPrice"]["changes"] = 0 963 964 if show: 965 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 966 967 else: 968 if show: 969 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 970 971 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
973 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 974 """ 975 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 976 977 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 978 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 979 :return: JSON formatted data with information about instrument. 980 """ 981 figiJSON = {} 982 if self.moreDebug: 983 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 984 985 if not self._figi: 986 uLogger.warning("self._figi variable is not be empty!") 987 988 else: 989 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 990 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 991 raise Exception("Instrument not allowed") 992 993 if not self.iList: 994 self.iList = self.Listing() 995 996 for item in self.iList["Shares"].keys(): 997 if self._figi == self.iList["Shares"][item]["figi"]: 998 figiJSON = self.iList["Shares"][item] 999 1000 if self.moreDebug: 1001 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1002 1003 break 1004 1005 if not figiJSON: 1006 for item in self.iList["Currencies"].keys(): 1007 if self._figi == self.iList["Currencies"][item]["figi"]: 1008 figiJSON = self.iList["Currencies"][item] 1009 1010 if self.moreDebug: 1011 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1012 1013 break 1014 1015 if not figiJSON: 1016 for item in self.iList["Bonds"].keys(): 1017 if self._figi == self.iList["Bonds"][item]["figi"]: 1018 figiJSON = self.iList["Bonds"][item] 1019 1020 if self.moreDebug: 1021 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1022 1023 break 1024 1025 if not figiJSON: 1026 for item in self.iList["Etfs"].keys(): 1027 if self._figi == self.iList["Etfs"][item]["figi"]: 1028 figiJSON = self.iList["Etfs"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Futures"].keys(): 1037 if self._figi == self.iList["Futures"][item]["figi"]: 1038 figiJSON = self.iList["Futures"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1042 1043 break 1044 1045 if figiJSON: 1046 self._figi = figiJSON["figi"] 1047 self._ticker = figiJSON["ticker"] 1048 1049 if requestPrice: 1050 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1051 1052 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1053 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1054 1055 else: 1056 figiJSON["currentPrice"]["changes"] = 0 1057 1058 if show: 1059 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1060 1061 else: 1062 if show: 1063 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1064 1065 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1067 def GetCurrentPrices(self, show: bool = True) -> dict: 1068 """ 1069 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1070 `{"buy": [{"price": 1243.8, "quantity": 193}, 1071 {"price": 1244.0, "quantity": 168}, 1072 {"price": 1244.8, "quantity": 5}, 1073 {"price": 1245.0, "quantity": 61}, 1074 {"price": 1245.4, "quantity": 60}], 1075 "sell": [{"price": 1243.6, "quantity": 8}, 1076 {"price": 1242.6, "quantity": 10}, 1077 {"price": 1242.4, "quantity": 18}, 1078 {"price": 1242.2, "quantity": 50}, 1079 {"price": 1242.0, "quantity": 113}], 1080 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1081 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1082 - sell: list of dicts with Buyers prices, 1083 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1084 - quantity: volume value by current price in lots, 1085 - limitUp: current trade session limit price, maximum, 1086 - limitDown: current trade session limit price, minimum, 1087 - lastPrice: last deal price of the instrument, 1088 - closePrice: previous trade session close price of the instrument. 1089 1090 See also: `SearchByTicker()` and `SearchByFIGI()`. 1091 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1092 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1093 1094 :param show: if `True` then print DOM to log and console. 1095 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1096 If an error occurred then returns an empty record: 1097 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1098 """ 1099 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1100 1101 if self.depth < 1: 1102 uLogger.error("Depth of Market (DOM) must be >=1!") 1103 raise Exception("Incorrect value") 1104 1105 if not (self._ticker or self._figi): 1106 uLogger.error("self._ticker or self._figi variables must be defined!") 1107 raise Exception("Ticker or FIGI required") 1108 1109 if self._ticker and not self._figi: 1110 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1111 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1112 1113 if not self._ticker and self._figi: 1114 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1115 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1116 1117 if not self._figi: 1118 uLogger.error("FIGI is not defined!") 1119 raise Exception("Ticker or FIGI required") 1120 1121 else: 1122 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1123 1124 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1125 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1126 self.body = str({"figi": self._figi, "depth": self.depth}) 1127 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1128 1129 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1130 # list of dicts with sellers orders: 1131 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1132 1133 # list of dicts with buyers orders: 1134 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1135 1136 # max price of instrument at this time: 1137 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1138 1139 # min price of instrument at this time: 1140 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1141 1142 # last price of deal with instrument: 1143 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1144 1145 # last close price of instrument: 1146 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1147 1148 else: 1149 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1150 uLogger.debug("Server response: {}".format(pricesResponse)) 1151 1152 if show: 1153 if prices["buy"] or prices["sell"]: 1154 info = [ 1155 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1156 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1157 self._ticker, 1158 self._figi, 1159 self.depth, 1160 ), 1161 "-" * 60, "\n", 1162 " Orders of Buyers | Orders of Sellers\n", 1163 "-" * 60, "\n", 1164 " Sell prices (volumes) | Buy prices (volumes)\n", 1165 "-" * 60, "\n", 1166 ] 1167 1168 if not prices["buy"]: 1169 info.append(" | No orders!\n") 1170 sumBuy = 0 1171 1172 else: 1173 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1174 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1175 for item in maxMinSorted: 1176 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1177 1178 if not prices["sell"]: 1179 info.append("No orders! |\n") 1180 sumSell = 0 1181 1182 else: 1183 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1184 for item in prices["sell"]: 1185 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1186 1187 info.extend([ 1188 "-" * 60, "\n", 1189 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1190 "-" * 60, "\n", 1191 ]) 1192 1193 infoText = "".join(info) 1194 1195 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1196 1197 else: 1198 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1199 1200 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1202 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1203 """ 1204 This method get and show information about all available broker instruments for current user account. 1205 If `instrumentsFile` string is not empty then also save information to this file. 1206 1207 :param show: if `True` then print results to console, if `False` — print only to file. 1208 :return: multi-lines string with all available broker instruments 1209 """ 1210 if not self.iList: 1211 self.iList = self.Listing() 1212 1213 info = [ 1214 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1215 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1216 ] 1217 1218 # add instruments count by type: 1219 for iType in self.iList.keys(): 1220 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1221 1222 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1223 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1224 1225 # generating info tables with all instruments by type: 1226 for iType in self.iList.keys(): 1227 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1228 1229 for instrument in self.iList[iType].keys(): 1230 iName = self.iList[iType][instrument]["name"] # instrument's name 1231 if len(iName) > 57: 1232 iName = "{}...".format(iName[:54]) # right trim for a long string 1233 1234 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1235 self.iList[iType][instrument]["ticker"], 1236 iName, 1237 self.iList[iType][instrument]["figi"], 1238 self.iList[iType][instrument]["currency"], 1239 self.iList[iType][instrument]["lot"], 1240 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1241 )) 1242 1243 infoText = "".join(info) 1244 1245 if show: 1246 uLogger.info(infoText) 1247 1248 if self.instrumentsFile: 1249 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1250 fH.write(infoText) 1251 1252 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1253 1254 if self.useHTMLReports: 1255 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1256 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1257 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1258 1259 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1260 1261 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1263 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1264 """ 1265 This method search and show information about instruments by part of its ticker, FIGI or name. 1266 If `searchResultsFile` string is not empty then also save information to this file. 1267 1268 :param pattern: string with part of ticker, FIGI or instrument's name. 1269 :param show: if `True` then print results to console, if `False` — return list of result only. 1270 :return: list of dictionaries with all found instruments. 1271 """ 1272 if not self.iList: 1273 self.iList = self.Listing() 1274 1275 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1276 compiledPattern = re.compile(pattern, re.IGNORECASE) 1277 1278 for iType in self.iList: 1279 for instrument in self.iList[iType].values(): 1280 searchResult = compiledPattern.search(" ".join( 1281 [instrument["ticker"], instrument["figi"], instrument["name"]] 1282 )) 1283 1284 if searchResult: 1285 searchResults[iType][instrument["ticker"]] = instrument 1286 1287 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1288 info = [ 1289 "# Search results\n\n", 1290 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile: 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 if self.useHTMLReports: 1342 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1343 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1344 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1345 1346 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1347 1348 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self._ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self._ticker = "" 1380 self._figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self._figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1395 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 1399 See limits: https://tinkoff.github.io/investAPI/limits/ 1400 1401 If `pricesFile` string is not empty then also save information to this file. 1402 1403 :param instruments: list of strings with tickers or FIGIs. 1404 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1405 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1406 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1407 """ 1408 if instruments is None or not instruments: 1409 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1410 raise Exception("Ticker or FIGI required") 1411 1412 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1413 1414 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1415 1416 iList = [] # trying to get info and current prices about all unique instruments: 1417 for self._figi in onlyUniqueFIGIs: 1418 iData = self.SearchByFIGI(requestPrice=True) 1419 iList.append(iData) 1420 1421 self.ShowListOfPrices(iList, show) 1422 1423 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1425 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1426 """ 1427 Show table contains current prices of given instruments. 1428 1429 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1430 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1431 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1432 :return: multilines text in Markdown format as a table contains current prices. 1433 """ 1434 infoText = "" 1435 1436 if show or self.pricesFile: 1437 info = [ 1438 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1439 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1440 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1441 ] 1442 1443 for item in iList: 1444 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1445 item["ticker"], 1446 item["figi"], 1447 item["type"], 1448 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1449 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1450 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1451 "{} / {}".format( 1452 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1453 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1454 ), 1455 "{} / {}".format( 1456 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1457 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1458 ), 1459 item["currency"], 1460 )) 1461 1462 infoText = "".join(info) 1463 1464 if show: 1465 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1466 1467 if self.pricesFile: 1468 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1469 fH.write(infoText) 1470 1471 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1472 1473 if self.useHTMLReports: 1474 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1475 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1476 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1477 1478 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1479 1480 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1482 def RequestTradingStatus(self) -> dict: 1483 """ 1484 Requesting trading status for the instrument defined by `figi` variable. 1485 1486 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1487 1488 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1489 1490 :return: dictionary with trading status attributes. Response example: 1491 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1492 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1493 """ 1494 if self._figi is None or not self._figi: 1495 uLogger.error("Variable `figi` must be defined for using this method!") 1496 raise Exception("FIGI required") 1497 1498 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1499 1500 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1501 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1502 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1503 1504 if self.moreDebug: 1505 uLogger.debug("Records about current trading status successfully received") 1506 1507 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1509 def RequestPortfolio(self) -> dict: 1510 """ 1511 Requesting actual user's portfolio for current `accountId`. 1512 1513 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1514 1515 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1516 1517 :return: dictionary with user's portfolio. 1518 """ 1519 if self.accountId is None or not self.accountId: 1520 uLogger.error("Variable `accountId` must be defined for using this method!") 1521 raise Exception("Account ID required") 1522 1523 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1524 1525 self.body = str({"accountId": self.accountId}) 1526 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1527 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1528 1529 if self.moreDebug: 1530 uLogger.debug("Records about user's portfolio successfully received") 1531 1532 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1534 def RequestPositions(self) -> dict: 1535 """ 1536 Requesting open positions by currencies and instruments for current `accountId`. 1537 1538 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1539 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending limit orders for current `accountId`. 1562 1563 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1564 1565 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1566 1567 :return: list of dictionaries with pending limit orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1577 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1578 1579 if "orders" in rawResponse.keys(): 1580 rawOrders = rawResponse["orders"] 1581 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1582 1583 else: 1584 rawOrders = [] 1585 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1586 1587 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1589 def RequestStopOrders(self) -> list: 1590 """ 1591 Requesting current actual stop orders for current `accountId`. 1592 1593 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1594 1595 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1596 1597 :return: list of dictionaries with stop orders. 1598 """ 1599 if self.accountId is None or not self.accountId: 1600 uLogger.error("Variable `accountId` must be defined for using this method!") 1601 raise Exception("Account ID required") 1602 1603 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1604 1605 self.body = str({"accountId": self.accountId}) 1606 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1607 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1608 1609 if "stopOrders" in rawResponse.keys(): 1610 rawStopOrders = rawResponse["stopOrders"] 1611 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1612 1613 else: 1614 rawStopOrders = [] 1615 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1616 1617 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1619 def Overview(self, show: bool = False, details: str = "full") -> dict: 1620 """ 1621 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1622 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1623 and `overviewBondsCalendarFile` are defined then also save information to file. 1624 1625 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1626 many requests about the state of the portfolio, and then, based on the received data, a large number 1627 of calculation and statistics are collected. 1628 1629 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1630 :param details: how detailed should the information be? 1631 - `full` — shows full available information about portfolio status (by default), 1632 - `positions` — shows only open positions, 1633 - `orders` — shows only sections of open limits and stop orders. 1634 - `digest` — show a short digest of the portfolio status, 1635 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1636 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1637 :return: dictionary with client's raw portfolio and some statistics. 1638 """ 1639 if self.accountId is None or not self.accountId: 1640 uLogger.error("Variable `accountId` must be defined for using this method!") 1641 raise Exception("Account ID required") 1642 1643 view = { 1644 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1645 "headers": {}, # list of dictionaries, response headers without "positions" section 1646 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1647 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1648 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1649 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1650 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1651 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1652 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1653 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1654 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1655 }, 1656 "stat": { # --- some statistics calculated using "raw" sections: 1657 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1658 "availableRUB": 0., # available rubles (without other currencies) 1659 "blockedRUB": 0., # blocked sum in Russian Rouble 1660 "totalChangesRUB": 0., # changes for all open trades in RUB 1661 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1662 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1663 "sharesCostRUB": 0., # costs of all shares in RUB 1664 "bondsCostRUB": 0., # costs of all bonds in RUB 1665 "etfsCostRUB": 0., # costs of all etfs in RUB 1666 "futuresCostRUB": 0., # costs of all futures in RUB 1667 "Currencies": [], # list of dictionaries of all currencies statistics 1668 "Shares": [], # list of dictionaries of all shares statistics 1669 "Bonds": [], # list of dictionaries of all bonds statistics 1670 "Etfs": [], # list of dictionaries of all etfs statistics 1671 "Futures": [], # list of dictionaries of all futures statistics 1672 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1673 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1674 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1675 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1676 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1677 }, 1678 "analytics": { # --- some analytics of portfolio: 1679 "distrByAssets": {}, # portfolio distribution by assets 1680 "distrByCompanies": {}, # portfolio distribution by companies 1681 "distrBySectors": {}, # portfolio distribution by sectors 1682 "distrByCurrencies": {}, # portfolio distribution by currencies 1683 "distrByCountries": {}, # portfolio distribution by countries 1684 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1685 } 1686 } 1687 1688 details = details.lower() 1689 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1690 if details not in availableDetails: 1691 details = "full" 1692 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1693 1694 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1695 1696 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1697 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1698 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1699 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1700 1701 # save response headers without "positions" section: 1702 for key in portfolioResponse.keys(): 1703 if key != "positions": 1704 view["raw"]["headers"][key] = portfolioResponse[key] 1705 1706 else: 1707 continue 1708 1709 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1710 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1711 for item in portfolioResponse["positions"]: 1712 if item["instrumentType"] == "currency": 1713 self._figi = item["figi"] 1714 if not self._figi and item["ticker"]: 1715 self._ticker = item["ticker"] 1716 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1717 1718 curr = self.SearchByFIGI(requestPrice=False) 1719 1720 # current price of currency in RUB: 1721 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1722 "name": curr["name"], 1723 "currentPrice": NanoToFloat( 1724 item["currentPrice"]["units"], 1725 item["currentPrice"]["nano"] 1726 ), 1727 } 1728 1729 view["raw"]["Currencies"].append(item) 1730 1731 elif item["instrumentType"] == "share": 1732 view["raw"]["Shares"].append(item) 1733 1734 elif item["instrumentType"] == "bond": 1735 view["raw"]["Bonds"].append(item) 1736 1737 elif item["instrumentType"] == "etf": 1738 view["raw"]["Etfs"].append(item) 1739 1740 elif item["instrumentType"] == "futures": 1741 view["raw"]["Futures"].append(item) 1742 1743 else: 1744 continue 1745 1746 # how many volume of currencies (by ISO currency name) are blocked: 1747 for item in view["raw"]["positions"]["blocked"]: 1748 blocked = NanoToFloat(item["units"], item["nano"]) 1749 if blocked > 0: 1750 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1751 1752 # how many volume of instruments (by FIGI) are blocked: 1753 for item in view["raw"]["positions"]["securities"]: 1754 blocked = int(item["blocked"]) 1755 if blocked > 0: 1756 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1757 1758 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1759 1760 if "rub" in allBlocked.keys(): 1761 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1762 1763 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1764 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1765 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1766 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1767 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1768 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1769 view["stat"]["portfolioCostRUB"] = sum([ 1770 view["stat"]["allCurrenciesCostRUB"], 1771 view["stat"]["sharesCostRUB"], 1772 view["stat"]["bondsCostRUB"], 1773 view["stat"]["etfsCostRUB"], 1774 view["stat"]["futuresCostRUB"], 1775 ]) 1776 1777 # --- calculating some portfolio statistics: 1778 byComp = {} # distribution by companies 1779 bySect = {} # distribution by sectors 1780 byCurr = {} # distribution by currencies (include RUB) 1781 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1782 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1783 1784 for item in portfolioResponse["positions"]: 1785 self._figi = item["figi"] 1786 if not self._figi and item["ticker"]: 1787 self._ticker = item["ticker"] 1788 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1789 1790 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1791 1792 if instrument: 1793 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1794 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1795 1796 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1797 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1798 1799 else: 1800 blocked = 0 1801 1802 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1803 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1804 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1805 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1806 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1807 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1808 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1809 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1810 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1811 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1812 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1813 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1814 1815 statData = { 1816 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1817 "ticker": instrument["ticker"], # ticker by FIGI 1818 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1819 "volume": volume, # available volume of instrument 1820 "lots": lots, # volume in lots of instrument 1821 "direction": direction, # direction of an instrument's position: short or long 1822 "blocked": blocked, # blocked volume of currency or instrument 1823 "currentPrice": curPrice, # current instrument's price in basic asset 1824 "average": average, # current average position price 1825 "cost": cost, # current cost of all volume of instrument in basic asset 1826 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1827 "costRUB": costRUB, # cost of instrument in ruble 1828 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1829 "profit": profit, # expected profit at current moment 1830 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1831 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1832 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1833 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1834 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1835 "step": instrument["step"], # minimum price increment 1836 } 1837 1838 # adding distribution by unique countries: 1839 if statData["country"] not in byCountry.keys(): 1840 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1841 1842 else: 1843 byCountry[statData["country"]]["cost"] += costRUB 1844 byCountry[statData["country"]]["percent"] += percentCostRUB 1845 1846 if item["instrumentType"] != "currency": 1847 # adding distribution by unique companies: 1848 if statData["name"]: 1849 if statData["name"] not in byComp.keys(): 1850 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1851 1852 else: 1853 byComp[statData["name"]]["cost"] += costRUB 1854 byComp[statData["name"]]["percent"] += percentCostRUB 1855 1856 # adding distribution by unique sectors: 1857 if statData["sector"] not in bySect.keys(): 1858 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1859 1860 else: 1861 bySect[statData["sector"]]["cost"] += costRUB 1862 bySect[statData["sector"]]["percent"] += percentCostRUB 1863 1864 # adding distribution by unique currencies: 1865 if currency not in byCurr.keys(): 1866 byCurr[currency] = { 1867 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1868 "cost": costRUB, 1869 "percent": percentCostRUB 1870 } 1871 1872 else: 1873 byCurr[currency]["cost"] += costRUB 1874 byCurr[currency]["percent"] += percentCostRUB 1875 1876 # saving statistics for every instrument: 1877 if item["instrumentType"] == "currency": 1878 view["stat"]["Currencies"].append(statData) 1879 1880 # update dict with free funds for trading (total - blocked) by currencies 1881 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1882 view["stat"]["funds"][currency] = { 1883 "total": volume, 1884 "totalCostRUB": costRUB, # total volume cost in rubles 1885 "free": volume - blocked, 1886 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1887 } 1888 1889 elif item["instrumentType"] == "share": 1890 view["stat"]["Shares"].append(statData) 1891 1892 elif item["instrumentType"] == "bond": 1893 view["stat"]["Bonds"].append(statData) 1894 1895 elif item["instrumentType"] == "etf": 1896 view["stat"]["Etfs"].append(statData) 1897 1898 elif item["instrumentType"] == "Futures": 1899 view["stat"]["Futures"].append(statData) 1900 1901 else: 1902 continue 1903 1904 # total changes in Russian Ruble: 1905 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1906 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1907 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1908 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1909 view["stat"]["funds"]["rub"] = { 1910 "total": view["stat"]["availableRUB"], 1911 "totalCostRUB": view["stat"]["availableRUB"], 1912 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1913 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1914 } 1915 1916 # --- pending limit orders sector data: 1917 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1918 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1919 1920 for item in view["raw"]["orders"]: 1921 self._figi = item["figi"] 1922 1923 if item["figi"] not in uniquePendingOrdersFIGIs: 1924 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1925 1926 uniquePendingOrdersFIGIs.append(item["figi"]) 1927 uniquePendingOrders[item["figi"]] = instrument 1928 1929 else: 1930 instrument = uniquePendingOrders[item["figi"]] 1931 1932 if instrument: 1933 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1934 orderType = TKS_ORDER_TYPES[item["orderType"]] 1935 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1936 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1937 1938 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1939 if item["direction"] == "ORDER_DIRECTION_BUY": 1940 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1941 1942 else: 1943 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1944 1945 # requested price for order execution: 1946 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1947 1948 # necessary changes in percent to reach target from current price: 1949 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1950 1951 view["stat"]["orders"].append({ 1952 "orderID": item["orderId"], # orderId number parameter of current order 1953 "figi": item["figi"], # FIGI identification 1954 "ticker": instrument["ticker"], # ticker name by FIGI 1955 "lotsRequested": item["lotsRequested"], # requested lots value 1956 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1957 "currentPrice": lastPrice, # current instrument's price for defined action 1958 "targetPrice": target, # requested price for order execution in base currency 1959 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1960 "percentChanges": changes, # changes in percent to target from current price 1961 "currency": item["currency"], # instrument's currency name 1962 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1963 "type": orderType, # type of order from TKS_ORDER_TYPES 1964 "status": orderState, # order status from TKS_ORDER_STATES 1965 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1966 }) 1967 1968 # --- stop orders sector data: 1969 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1970 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1971 1972 for item in view["raw"]["stopOrders"]: 1973 self._figi = item["figi"] 1974 1975 if item["figi"] not in uniqueStopOrdersFIGIs: 1976 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1977 1978 uniqueStopOrdersFIGIs.append(item["figi"]) 1979 uniqueStopOrders[item["figi"]] = instrument 1980 1981 else: 1982 instrument = uniqueStopOrders[item["figi"]] 1983 1984 if instrument: 1985 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1986 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1987 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1988 1989 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1990 if "expirationTime" in item.keys(): 1991 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1992 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1993 1994 else: 1995 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1996 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1997 1998 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1999 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2000 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2001 2002 else: 2003 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2004 2005 # requested price when stop-order executed: 2006 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2007 2008 # price for limit-order, set up when stop-order executed: 2009 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2010 2011 # necessary changes in percent to reach target from current price: 2012 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2013 2014 view["stat"]["stopOrders"].append({ 2015 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2016 "figi": item["figi"], # FIGI identification 2017 "ticker": instrument["ticker"], # ticker name by FIGI 2018 "lotsRequested": item["lotsRequested"], # requested lots value 2019 "currentPrice": lastPrice, # current instrument's price for defined action 2020 "targetPrice": target, # requested price for stop-order execution in base currency 2021 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2022 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2023 "percentChanges": changes, # changes in percent to target from current price 2024 "currency": item["currency"], # instrument's currency name 2025 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2026 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2027 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2028 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2029 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2030 }) 2031 2032 # --- calculating data for analytics section: 2033 # portfolio distribution by assets: 2034 view["analytics"]["distrByAssets"] = { 2035 "Ruble": { 2036 "uniques": 1, 2037 "cost": view["stat"]["availableRUB"], 2038 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2039 }, 2040 "Currencies": { 2041 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2042 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2043 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2044 }, 2045 "Shares": { 2046 "uniques": len(view["stat"]["Shares"]), 2047 "cost": view["stat"]["sharesCostRUB"], 2048 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2049 }, 2050 "Bonds": { 2051 "uniques": len(view["stat"]["Bonds"]), 2052 "cost": view["stat"]["bondsCostRUB"], 2053 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2054 }, 2055 "Etfs": { 2056 "uniques": len(view["stat"]["Etfs"]), 2057 "cost": view["stat"]["etfsCostRUB"], 2058 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2059 }, 2060 "Futures": { 2061 "uniques": len(view["stat"]["Futures"]), 2062 "cost": view["stat"]["futuresCostRUB"], 2063 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2064 }, 2065 } 2066 2067 # portfolio distribution by companies: 2068 view["analytics"]["distrByCompanies"]["All money cash"] = { 2069 "ticker": "", 2070 "cost": view["stat"]["allCurrenciesCostRUB"], 2071 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2072 } 2073 view["analytics"]["distrByCompanies"].update(byComp) 2074 2075 # portfolio distribution by sectors: 2076 view["analytics"]["distrBySectors"]["All money cash"] = { 2077 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2078 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2079 } 2080 view["analytics"]["distrBySectors"].update(bySect) 2081 2082 # portfolio distribution by currencies: 2083 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2084 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2085 2086 if self.moreDebug: 2087 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2088 2089 view["analytics"]["distrByCurrencies"].update(byCurr) 2090 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2091 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2092 2093 # portfolio distribution by countries: 2094 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2096 2097 if self.moreDebug: 2098 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2099 2100 view["analytics"]["distrByCountries"].update(byCountry) 2101 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2102 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2103 2104 # --- Prepare text statistics overview in human-readable: 2105 if show: 2106 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2107 2108 # Whatever the value `details`, header not changes: 2109 info = [ 2110 "# Client's portfolio\n\n", 2111 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2112 "* **Account ID:** [{}]\n".format(self.accountId), 2113 ] 2114 2115 if details in ["full", "positions", "digest"]: 2116 info.extend([ 2117 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2118 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2119 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2120 view["stat"]["totalChangesRUB"], 2121 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2122 view["stat"]["totalChangesPercentRUB"], 2123 ), 2124 ]) 2125 2126 if details in ["full", "positions"]: 2127 info.extend([ 2128 "## Open positions\n\n", 2129 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2130 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2131 "| **Ruble:** | {:>31} | | | | | |\n".format( 2132 "{:.2f} ({:.2f}) rub".format( 2133 view["stat"]["availableRUB"], 2134 view["stat"]["blockedRUB"], 2135 ) 2136 ) 2137 ]) 2138 2139 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2140 return [ 2141 "| | | | | | | |\n", 2142 "| {:<27} | | | | | {:>19} | |\n".format( 2143 noTradeStr if noTradeStr else typeStr, 2144 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2145 ), 2146 ] 2147 2148 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2149 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2150 "{} [{}]".format(data["ticker"], data["figi"]), 2151 "{:.2f} ({:.2f}) {}".format( 2152 data["volume"], 2153 data["blocked"], 2154 data["currency"], 2155 ) if isCurr else "{:.0f} ({:.0f})".format( 2156 data["volume"], 2157 data["blocked"], 2158 ), 2159 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2160 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2161 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2162 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2163 "{}{:.2f} {} ({}{:.2f}%)".format( 2164 "+" if data["profit"] > 0 else "", 2165 data["profit"], data["baseCurrencyName"], 2166 "+" if data["percentProfit"] > 0 else "", 2167 data["percentProfit"], 2168 ), 2169 ) 2170 2171 # --- Show currencies section: 2172 if view["stat"]["Currencies"]: 2173 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2174 for item in view["stat"]["Currencies"]: 2175 info.append(_InfoStr(item, isCurr=True)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2179 2180 # --- Show shares section: 2181 if view["stat"]["Shares"]: 2182 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2183 2184 for item in view["stat"]["Shares"]: 2185 info.append(_InfoStr(item)) 2186 2187 else: 2188 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2189 2190 # --- Show bonds section: 2191 if view["stat"]["Bonds"]: 2192 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2193 2194 for item in view["stat"]["Bonds"]: 2195 info.append(_InfoStr(item)) 2196 2197 else: 2198 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2199 2200 # --- Show etfs section: 2201 if view["stat"]["Etfs"]: 2202 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2203 2204 for item in view["stat"]["Etfs"]: 2205 info.append(_InfoStr(item)) 2206 2207 else: 2208 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2209 2210 # --- Show futures section: 2211 if view["stat"]["Futures"]: 2212 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2213 2214 for item in view["stat"]["Futures"]: 2215 info.append(_InfoStr(item)) 2216 2217 else: 2218 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2219 2220 if details in ["full", "orders"]: 2221 # --- Show pending limit orders section: 2222 if view["stat"]["orders"]: 2223 info.extend([ 2224 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2225 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2226 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2227 ]) 2228 2229 for item in view["stat"]["orders"]: 2230 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2231 "{} [{}]".format(item["ticker"], item["figi"]), 2232 item["orderID"], 2233 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2234 "{} {} ({}{:.2f}%)".format( 2235 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2236 item["baseCurrencyName"], 2237 "+" if item["percentChanges"] > 0 else "", 2238 float(item["percentChanges"]), 2239 ), 2240 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2241 item["action"], 2242 item["type"], 2243 item["date"], 2244 )) 2245 2246 else: 2247 info.append("\n## Total pending limit-orders: [0]\n") 2248 2249 # --- Show stop orders section: 2250 if view["stat"]["stopOrders"]: 2251 info.extend([ 2252 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2253 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2254 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2255 ]) 2256 2257 for item in view["stat"]["stopOrders"]: 2258 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2259 "{} [{}]".format(item["ticker"], item["figi"]), 2260 item["orderID"], 2261 item["lotsRequested"], 2262 "{} {} ({}{:.2f}%)".format( 2263 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2264 item["baseCurrencyName"], 2265 "+" if item["percentChanges"] > 0 else "", 2266 float(item["percentChanges"]), 2267 ), 2268 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2269 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2270 item["action"], 2271 item["type"], 2272 item["expType"], 2273 item["createDate"], 2274 item["expDate"], 2275 )) 2276 2277 else: 2278 info.append("\n## Total stop-orders: [0]\n") 2279 2280 if details in ["full", "analytics"]: 2281 # -- Show analytics section: 2282 if view["stat"]["portfolioCostRUB"] > 0: 2283 info.extend([ 2284 "\n# Analytics\n\n" 2285 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2286 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2287 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2288 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2289 view["stat"]["totalChangesRUB"], 2290 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2291 view["stat"]["totalChangesPercentRUB"], 2292 ), 2293 "\n## Portfolio distribution by assets\n" 2294 "\n| Type | Uniques | Percent | Current cost |\n", 2295 "|------------------------------------|---------|---------|--------------------|\n", 2296 ]) 2297 2298 for key in view["analytics"]["distrByAssets"].keys(): 2299 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2300 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2301 key, 2302 view["analytics"]["distrByAssets"][key]["uniques"], 2303 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2304 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2305 )) 2306 2307 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2308 2309 info.extend([ 2310 "\n## Portfolio distribution by companies\n" 2311 "\n| Company | Percent | Current cost |\n", 2312 aSepLine, 2313 ]) 2314 2315 for company in view["analytics"]["distrByCompanies"].keys(): 2316 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2317 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2318 "{}{}".format( 2319 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2320 company, 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2324 )) 2325 2326 info.extend([ 2327 "\n## Portfolio distribution by sectors\n" 2328 "\n| Sector | Percent | Current cost |\n", 2329 aSepLine, 2330 ]) 2331 2332 for sector in view["analytics"]["distrBySectors"].keys(): 2333 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2334 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2335 sector, 2336 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2338 )) 2339 2340 info.extend([ 2341 "\n## Portfolio distribution by currencies\n" 2342 "\n| Instruments currencies | Percent | Current cost |\n", 2343 aSepLine, 2344 ]) 2345 2346 for curr in view["analytics"]["distrByCurrencies"].keys(): 2347 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2348 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2349 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2350 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2351 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2352 )) 2353 2354 info.extend([ 2355 "\n## Portfolio distribution by countries\n" 2356 "\n| Assets by country | Percent | Current cost |\n", 2357 aSepLine, 2358 ]) 2359 2360 for country in view["analytics"]["distrByCountries"].keys(): 2361 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2362 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2363 country, 2364 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2365 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2366 )) 2367 2368 if details in ["full", "calendar"]: 2369 # -- Show bonds payment calendar section: 2370 if view["stat"]["Bonds"]: 2371 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2372 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2373 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2374 2375 else: 2376 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2377 2378 infoText = "".join(info) 2379 2380 uLogger.info(infoText) 2381 2382 if details == "full" and self.overviewFile: 2383 filename = self.overviewFile 2384 2385 elif details == "digest" and self.overviewDigestFile: 2386 filename = self.overviewDigestFile 2387 2388 elif details == "positions" and self.overviewPositionsFile: 2389 filename = self.overviewPositionsFile 2390 2391 elif details == "orders" and self.overviewOrdersFile: 2392 filename = self.overviewOrdersFile 2393 2394 elif details == "analytics" and self.overviewAnalyticsFile: 2395 filename = self.overviewAnalyticsFile 2396 2397 elif details == "calendar" and self.overviewBondsCalendarFile: 2398 filename = self.overviewBondsCalendarFile 2399 2400 else: 2401 filename = "" 2402 2403 if filename: 2404 with open(filename, "w", encoding="UTF-8") as fH: 2405 fH.write(infoText) 2406 2407 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2408 2409 if self.useHTMLReports: 2410 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2411 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2412 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2413 2414 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2415 2416 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2418 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2419 """ 2420 Returns history operations between two given dates for current `accountId`. 2421 If `reportFile` string is not empty then also save human-readable report. 2422 Shows some statistical data of closed positions. 2423 2424 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2425 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2426 :param show: if `True` then also prints all records to the console. 2427 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2428 :return: original list of dictionaries with history of deals records from API ("operations" key): 2429 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2430 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2431 """ 2432 if self.accountId is None or not self.accountId: 2433 uLogger.error("Variable `accountId` must be defined for using this method!") 2434 raise Exception("Account ID required") 2435 2436 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2437 2438 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2439 2440 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2441 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2442 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2443 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2444 customStat = {} # custom statistics in additional to responseJSON 2445 2446 # --- output report in human-readable format: 2447 if show or self.reportFile: 2448 splitLine1 = "| | | | | |\n" # Summary section 2449 splitLine2 = "| | | | | | | | |\n" # Operations section 2450 nextDay = "" 2451 2452 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2453 2454 if len(ops) > 0: 2455 customStat = { 2456 "opsCount": 0, # total operations count 2457 "buyCount": 0, # buy operations 2458 "sellCount": 0, # sell operations 2459 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2460 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2461 "payIn": {"rub": 0.}, # Deposit brokerage account 2462 "payOut": {"rub": 0.}, # Withdrawals 2463 "divs": {"rub": 0.}, # Dividends income 2464 "coupons": {"rub": 0.}, # Coupon's income 2465 "brokerCom": {"rub": 0.}, # Service commissions 2466 "serviceCom": {"rub": 0.}, # Service commissions 2467 "marginCom": {"rub": 0.}, # Margin commissions 2468 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2469 } 2470 2471 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2472 for item in ops: 2473 if item["state"] == "OPERATION_STATE_EXECUTED": 2474 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2475 2476 # count buy operations: 2477 if "_BUY" in item["operationType"]: 2478 customStat["buyCount"] += 1 2479 2480 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2481 customStat["buyTotal"][item["payment"]["currency"]] += payment 2482 2483 else: 2484 customStat["buyTotal"][item["payment"]["currency"]] = payment 2485 2486 # count sell operations: 2487 elif "_SELL" in item["operationType"]: 2488 customStat["sellCount"] += 1 2489 2490 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2491 customStat["sellTotal"][item["payment"]["currency"]] += payment 2492 2493 else: 2494 customStat["sellTotal"][item["payment"]["currency"]] = payment 2495 2496 # count incoming operations: 2497 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2498 if item["payment"]["currency"] in customStat["payIn"].keys(): 2499 customStat["payIn"][item["payment"]["currency"]] += payment 2500 2501 else: 2502 customStat["payIn"][item["payment"]["currency"]] = payment 2503 2504 # count withdrawals operations: 2505 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2506 if item["payment"]["currency"] in customStat["payOut"].keys(): 2507 customStat["payOut"][item["payment"]["currency"]] += payment 2508 2509 else: 2510 customStat["payOut"][item["payment"]["currency"]] = payment 2511 2512 # count dividends income: 2513 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2514 if item["payment"]["currency"] in customStat["divs"].keys(): 2515 customStat["divs"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["divs"][item["payment"]["currency"]] = payment 2519 2520 # count coupon's income: 2521 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2522 if item["payment"]["currency"] in customStat["coupons"].keys(): 2523 customStat["coupons"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["coupons"][item["payment"]["currency"]] = payment 2527 2528 # count broker commissions: 2529 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2530 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2531 customStat["brokerCom"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["brokerCom"][item["payment"]["currency"]] = payment 2535 2536 # count service commissions: 2537 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2538 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2539 customStat["serviceCom"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["serviceCom"][item["payment"]["currency"]] = payment 2543 2544 # count margin commissions: 2545 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2546 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2547 customStat["marginCom"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["marginCom"][item["payment"]["currency"]] = payment 2551 2552 # count withholding taxes: 2553 elif "_TAX" in item["operationType"]: 2554 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2555 customStat["allTaxes"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["allTaxes"][item["payment"]["currency"]] = payment 2559 2560 else: 2561 continue 2562 2563 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2564 2565 # --- view "Actions" lines: 2566 info.extend([ 2567 "| Report sections | | | | |\n", 2568 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2569 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2570 "| | Buy: {:<22} | {:<28} | | |\n".format( 2571 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2572 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2573 ), 2574 "| | Sell: {:<21} | {:<28} | | |\n".format( 2575 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2576 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2577 ), 2578 ]) 2579 2580 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2581 for key in opsKeys: 2582 if key == "rub": 2583 continue 2584 2585 info.extend([ 2586 "| | | {:<28} | | |\n".format( 2587 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2588 ), 2589 "| | | {:<28} | | |\n".format( 2590 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2591 ), 2592 ]) 2593 2594 info.append(splitLine1) 2595 2596 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2597 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2598 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2601 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2602 ) 2603 2604 # --- view "Payments" lines: 2605 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2606 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2607 2608 for key in paymentsKeys: 2609 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2610 2611 info.append(splitLine1) 2612 2613 # --- view "Commissions and taxes" lines: 2614 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2615 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2616 2617 for key in comKeys: 2618 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2619 2620 info.extend([ 2621 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2622 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2623 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2624 ]) 2625 2626 else: 2627 info.append("Broker returned no operations during this period\n") 2628 2629 # --- view "Operations" section: 2630 for item in ops: 2631 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2632 continue 2633 2634 else: 2635 self._figi = item["figi"] 2636 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2637 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2638 2639 # group of deals during one day: 2640 if nextDay and item["date"].split("T")[0] != nextDay: 2641 info.append(splitLine2) 2642 nextDay = "" 2643 2644 else: 2645 nextDay = item["date"].split("T")[0] # saving current day for splitting 2646 2647 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2648 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2649 self._figi if self._figi else "—", 2650 instrument["ticker"] if instrument else "—", 2651 instrument["type"] if instrument else "—", 2652 item["quantity"] if int(item["quantity"]) > 0 else "—", 2653 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2654 TKS_OPERATION_STATES[item["state"]], 2655 TKS_OPERATION_TYPES[item["operationType"]], 2656 )) 2657 2658 infoText = "".join(info) 2659 2660 if show: 2661 if self.moreDebug: 2662 uLogger.debug("Records about history of a client's operations successfully received") 2663 2664 uLogger.info(infoText) 2665 2666 if self.reportFile: 2667 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2668 fH.write(infoText) 2669 2670 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2671 2672 if self.useHTMLReports: 2673 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2674 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2675 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2676 2677 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2678 2679 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2681 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2682 """ 2683 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2684 2685 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2686 Warning! Broker server used ISO UTC time by default. 2687 2688 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2689 Also, `historyFile` used to update history with `onlyMissing` parameter. 2690 2691 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2692 2693 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2695 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2696 `"hour"`, `"day"`. Default: `"hour"`. 2697 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2698 False by default. Warning! History appends only from last candle to current time 2699 with always update last candle! 2700 :param csvSep: separator if csv-file is used, `,` by default. 2701 :param show: if `True` then also prints Pandas DataFrame to the console. 2702 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2703 `["date", "time", "open", "high", "low", "close", "volume"]`. 2704 """ 2705 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2706 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2707 history = None # empty pandas object for history 2708 2709 if interval not in TKS_CANDLE_INTERVALS.keys(): 2710 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2711 raise Exception("Incorrect value") 2712 2713 if not (self._ticker or self._figi): 2714 uLogger.error("Ticker or FIGI must be defined!") 2715 raise Exception("Ticker or FIGI required") 2716 2717 if self._ticker and not self._figi: 2718 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2719 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2720 2721 if self._figi and not self._ticker: 2722 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2723 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2724 2725 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2726 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2727 if interval.lower() != "day": 2728 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2729 2730 delta = dtEnd - dtStart # current UTC time minus last time in file 2731 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2732 2733 # calculate history length in candles: 2734 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2735 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2736 length += 1 # to avoid fraction time 2737 2738 # calculate data blocks count: 2739 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2740 2741 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2742 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2743 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2744 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2745 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2746 2747 tempOld = None # pandas object for old history, if --only-missing key present 2748 lastTime = None # datetime object of last old candle in file 2749 2750 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2751 uLogger.debug("--only-missing key present, add only last missing candles...") 2752 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2753 2754 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2755 2756 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2757 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2758 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2759 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2760 2761 # get last datetime object from last string in file or minus 1 delta if file is empty: 2762 if len(tempOld) > 0: 2763 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2764 2765 else: 2766 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2767 2768 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2769 2770 responseJSONs = [] # raw history blocks of data 2771 2772 blockEnd = dtEnd 2773 for item in range(blocks): 2774 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2775 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2776 2777 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2778 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2779 )) 2780 2781 if blockStart == blockEnd: 2782 uLogger.debug("Skipped this zero-length block...") 2783 2784 else: 2785 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2786 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2787 self.body = str({ 2788 "figi": self._figi, 2789 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2790 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2791 "interval": TKS_CANDLE_INTERVALS[interval][0] 2792 }) 2793 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2794 2795 if "code" in responseJSON.keys(): 2796 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2797 2798 else: 2799 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2800 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2801 2802 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2803 2804 blockEnd = blockStart 2805 2806 printCount = len(responseJSONs) # candles to show in console 2807 if responseJSONs: 2808 tempHistory = pd.DataFrame( 2809 data={ 2810 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2811 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2812 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2813 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2814 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2815 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2816 "volume": [int(item["volume"]) for item in responseJSONs], 2817 }, 2818 index=range(len(responseJSONs)), 2819 columns=["date", "time", "open", "high", "low", "close", "volume"], 2820 ) 2821 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2822 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2823 2824 # append only newest candles to old history if --only-missing key present: 2825 if onlyMissing and tempOld is not None and lastTime is not None: 2826 index = 0 # find start index in tempHistory data: 2827 2828 for i, item in tempHistory.iterrows(): 2829 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2830 2831 if curTime == lastTime: 2832 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2833 index = i 2834 printCount = index + 1 2835 break 2836 2837 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2838 2839 else: 2840 history = tempHistory # if no `--only-missing` key then load full data from server 2841 2842 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2843 2844 if history is not None and not history.empty: 2845 if show: 2846 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2847 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2848 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2849 )) 2850 2851 else: 2852 uLogger.warning("Received an empty candles history!") 2853 2854 if self.historyFile is not None: 2855 if history is not None and not history.empty: 2856 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2857 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2858 2859 else: 2860 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2861 2862 else: 2863 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2864 2865 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2867 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2868 """ 2869 Load candles history from csv-file and return Pandas DataFrame object. 2870 2871 See also: `History()` and `ShowHistoryChart()` methods. 2872 2873 :param filePath: path to csv-file to open. 2874 """ 2875 loadedHistory = None # init candles data object 2876 2877 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2878 2879 if os.path.exists(filePath): 2880 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2881 2882 tfStr = self.priceModel.FormattedDelta( 2883 self.priceModel.timeframe, 2884 "{days} days {hours}h {minutes}m {seconds}s", 2885 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2886 self.priceModel.timeframe, 2887 "{hours}h {minutes}m {seconds}s", 2888 ) 2889 2890 if loadedHistory is not None and not loadedHistory.empty: 2891 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2892 len(loadedHistory), 2893 tfStr, 2894 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2895 ) 2896 2897 else: 2898 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2899 2900 else: 2901 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2902 2903 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2905 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2906 """ 2907 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2908 2909 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2910 Default: `index.html` (both for interact and non-interact candlesticks chart). 2911 2912 See also: `History()` and `LoadHistory()` methods. 2913 2914 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2915 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2916 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2917 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2918 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2919 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2920 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2921 """ 2922 if isinstance(candles, str): 2923 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2924 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2925 2926 elif isinstance(candles, pd.DataFrame): 2927 self.priceModel.prices = candles # set candles chain from variable 2928 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2929 2930 if "datetime" not in candles.columns: 2931 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2932 2933 else: 2934 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2935 raise Exception("Incorrect value") 2936 2937 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2938 2939 if interact: 2940 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2941 2942 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2943 2944 else: 2945 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2946 2947 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2948 2949 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2951 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2952 """ 2953 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2954 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2955 2956 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2957 2958 :param operation: string "Buy" or "Sell". 2959 :param lots: volume, integer count of lots >= 1. 2960 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2961 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2962 :param expDate: string "Undefined" by default or local date in future, 2963 it is a string with format `%Y-%m-%d %H:%M:%S`. 2964 :return: JSON with response from broker server. 2965 """ 2966 if self.accountId is None or not self.accountId: 2967 uLogger.error("Variable `accountId` must be defined for using this method!") 2968 raise Exception("Account ID required") 2969 2970 if operation is None or not operation or operation not in ("Buy", "Sell"): 2971 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2972 raise Exception("Incorrect value") 2973 2974 if lots is None or lots < 1: 2975 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2976 lots = 1 2977 2978 if tp is None or tp < 0: 2979 tp = 0 2980 2981 if sl is None or sl < 0: 2982 sl = 0 2983 2984 if expDate is None or not expDate: 2985 expDate = "Undefined" 2986 2987 if not (self._ticker or self._figi): 2988 uLogger.error("Ticker or FIGI must be defined!") 2989 raise Exception("Ticker or FIGI required") 2990 2991 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2992 self._ticker = instrument["ticker"] 2993 self._figi = instrument["figi"] 2994 2995 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2996 2997 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2998 self.body = str({ 2999 "figi": self._figi, 3000 "quantity": str(lots), 3001 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3002 "accountId": str(self.accountId), 3003 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3004 }) 3005 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3006 3007 if "orderId" in response.keys(): 3008 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3009 operation, response["orderId"], 3010 self._ticker, self._figi, lots, 3011 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3012 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3013 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3014 )) 3015 3016 if tp > 0: 3017 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3018 3019 if sl > 0: 3020 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3021 3022 else: 3023 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3024 3025 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3027 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3028 """ 3029 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3030 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3031 3032 See also: `Order()` and `Trade()` docstrings. 3033 3034 :param lots: volume, integer count of lots >= 1. 3035 :param tp: float > 0, take profit price of stop-order. 3036 :param sl: float > 0, stop loss price of stop-order. 3037 :param expDate: it's a local date in future. 3038 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3039 :return: JSON with response from broker server. 3040 """ 3041 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3043 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3044 """ 3045 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3046 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3047 3048 See also: `Order()` and `Trade()` docstrings. 3049 3050 :param lots: volume, integer count of lots >= 1. 3051 :param tp: float > 0, take profit price of stop-order. 3052 :param sl: float > 0, stop loss price of stop-order. 3053 :param expDate: it's a local date in the future. 3054 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3055 :return: JSON with response from broker server. 3056 """ 3057 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3059 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3060 """ 3061 Close position of given instruments. 3062 3063 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3064 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3065 This avoids unnecessary downloading data from the server. 3066 """ 3067 if instruments is None or not instruments: 3068 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3069 raise Exception("Ticker or FIGI required") 3070 3071 if isinstance(instruments, str): 3072 instruments = [instruments] 3073 3074 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3075 if uniqueInstruments: 3076 if portfolio is None or not portfolio: 3077 portfolio = self.Overview(show=False) 3078 3079 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3080 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3081 3082 for self._figi in uniqueInstruments: 3083 if self._figi not in allOpened: 3084 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3085 continue 3086 3087 # search open trade info about instrument by ticker: 3088 instrument = {} 3089 for iType in TKS_INSTRUMENTS: 3090 if instrument: 3091 break 3092 3093 for item in portfolio["stat"][iType]: 3094 if item["figi"] == self._figi: 3095 instrument = item 3096 break 3097 3098 if instrument: 3099 self._ticker = instrument["ticker"] 3100 self._figi = instrument["figi"] 3101 3102 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3103 self._ticker, 3104 self._figi, 3105 int(instrument["volume"]), 3106 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3107 )) 3108 3109 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3110 3111 if tradeLots > 0: 3112 if instrument["blocked"] > 0: 3113 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3114 instrument["blocked"], 3115 self._ticker, 3116 tradeLots, 3117 )) 3118 3119 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3120 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3121 3122 else: 3123 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3125 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3126 """ 3127 Close all positions of given instruments with defined type. 3128 3129 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3130 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3131 This avoids unnecessary downloading data from the server. 3132 """ 3133 if iType not in TKS_INSTRUMENTS: 3134 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3135 3136 else: 3137 if portfolio is None or not portfolio: 3138 portfolio = self.Overview(show=False) 3139 3140 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3141 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3142 3143 if tickers and portfolio: 3144 self.CloseTrades(tickers, portfolio) 3145 3146 else: 3147 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3149 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3150 """ 3151 Universal method to create market or limit orders with all available parameters for current `accountId`. 3152 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3153 3154 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3155 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3156 3157 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3158 then broker immediately open market order as you can do simple --buy or --sell operations! 3159 3160 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3161 When current price will go up or down to target price value then broker opens a limit order. 3162 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3163 3164 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3165 3166 :param operation: string "Buy" or "Sell". 3167 :param orderType: string "Limit" or "Stop". 3168 :param lots: volume, integer count of lots >= 1. 3169 :param targetPrice: target price > 0. This is open trade price for limit order. 3170 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3171 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3172 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3173 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3174 Stop loss order always executed by market price. 3175 :param expDate: string "Undefined" by default or local date in future. 3176 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3177 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3178 A limit order has no expiration date, it lasts until the end of the trading day. 3179 :return: JSON with response from broker server. 3180 """ 3181 if self.accountId is None or not self.accountId: 3182 uLogger.error("Variable `accountId` must be defined for using this method!") 3183 raise Exception("Account ID required") 3184 3185 if operation is None or not operation or operation not in ("Buy", "Sell"): 3186 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3187 raise Exception("Incorrect value") 3188 3189 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3190 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3191 raise Exception("Incorrect value") 3192 3193 if lots is None or lots < 1: 3194 uLogger.error("You must define trade volume > 0: integer count of lots!") 3195 raise Exception("Incorrect value") 3196 3197 if targetPrice is None or targetPrice <= 0: 3198 uLogger.error("Target price for limit-order must be greater than 0!") 3199 raise Exception("Incorrect value") 3200 3201 if limitPrice is None or limitPrice <= 0: 3202 limitPrice = targetPrice 3203 3204 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3205 stopType = "Limit" 3206 3207 if expDate is None or not expDate: 3208 expDate = "Undefined" 3209 3210 if not (self._ticker or self._figi): 3211 uLogger.error("Tocker or FIGI must be defined!") 3212 raise Exception("Ticker or FIGI required") 3213 3214 response = {} 3215 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3216 self._ticker = instrument["ticker"] 3217 self._figi = instrument["figi"] 3218 3219 if orderType == "Limit": 3220 uLogger.debug( 3221 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3222 self._ticker, self._figi, 3223 operation, lots, targetPrice, instrument["currency"], 3224 )) 3225 3226 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3227 self.body = str({ 3228 "figi": self._figi, 3229 "quantity": str(lots), 3230 "price": FloatToNano(targetPrice), 3231 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3232 "accountId": str(self.accountId), 3233 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3234 }) 3235 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3236 3237 if "orderId" in response.keys(): 3238 uLogger.info( 3239 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3240 response["orderId"], self._ticker, self._figi, operation, lots, 3241 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3242 )) 3243 3244 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3245 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3246 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3247 targetPrice, instrument["currency"], 3248 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3249 )) 3250 3251 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3252 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3253 targetPrice, instrument["currency"], 3254 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3255 )) 3256 3257 else: 3258 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3259 3260 if orderType == "Stop": 3261 uLogger.debug( 3262 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3263 self._ticker, self._figi, 3264 operation, lots, 3265 targetPrice, instrument["currency"], 3266 limitPrice, instrument["currency"], 3267 stopType, expDate, 3268 )) 3269 3270 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3271 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3272 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3273 3274 body = { 3275 "figi": self._figi, 3276 "quantity": str(lots), 3277 "price": FloatToNano(limitPrice), 3278 "stopPrice": FloatToNano(targetPrice), 3279 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3280 "accountId": str(self.accountId), 3281 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3282 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3283 } 3284 3285 if expDateUTC: 3286 body["expireDate"] = expDateUTC 3287 3288 self.body = str(body) 3289 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3290 3291 if "stopOrderId" in response.keys(): 3292 uLogger.info( 3293 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3294 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3295 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3296 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3297 TKS_STOP_ORDER_TYPES[stopOrderType], 3298 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3299 )) 3300 3301 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3302 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3303 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3304 targetPrice, instrument["currency"], 3305 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3306 )) 3307 3308 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3309 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3310 targetPrice, instrument["currency"], 3311 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3312 )) 3313 3314 else: 3315 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3316 3317 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3319 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3320 """ 3321 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3322 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3323 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3324 See also: `Order()` docstring. 3325 3326 :param lots: volume, integer count of lots >= 1. 3327 :param targetPrice: target price > 0. This is open trade price for limit order. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3332 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3333 """ 3334 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3335 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3336 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3337 target price value then broker opens a limit order. See also: `Order()` docstring. 3338 3339 :param lots: volume, integer count of lots >= 1. 3340 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3341 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3342 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3343 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3344 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3345 :param expDate: string "Undefined" by default or local date in future. 3346 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3347 This date is converting to UTC format for server. 3348 :return: JSON with response from broker server. 3349 """ 3350 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3352 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3353 """ 3354 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3355 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3356 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3357 See also: `Order()` docstring. 3358 3359 :param lots: volume, integer count of lots >= 1. 3360 :param targetPrice: target price > 0. This is open trade price for limit order. 3361 :return: JSON with response from broker server. 3362 """ 3363 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3365 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3366 """ 3367 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3368 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3369 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3370 target price value then broker opens a limit order. See also: `Order()` docstring. 3371 3372 :param lots: volume, integer count of lots >= 1. 3373 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3374 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3375 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3376 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3377 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3378 :param expDate: string "Undefined" by default or local date in future. 3379 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3380 This date is converting to UTC format for server. 3381 :return: JSON with response from broker server. 3382 """ 3383 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3385 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3386 """ 3387 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3388 3389 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3390 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3391 This avoids unnecessary downloading data from the server. 3392 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3393 """ 3394 if self.accountId is None or not self.accountId: 3395 uLogger.error("Variable `accountId` must be defined for using this method!") 3396 raise Exception("Account ID required") 3397 3398 if orderIDs: 3399 if allOrdersIDs is None: 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3402 3403 if allStopOrdersIDs is None: 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 3407 for orderID in orderIDs: 3408 idInPendingOrders = orderID in allOrdersIDs 3409 idInStopOrders = orderID in allStopOrdersIDs 3410 3411 if not (idInPendingOrders or idInStopOrders): 3412 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3413 continue 3414 3415 else: 3416 if idInPendingOrders: 3417 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3418 3419 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3420 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3421 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3422 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3423 3424 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3425 if self.moreDebug: 3426 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3427 3428 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3429 3430 else: 3431 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3432 3433 elif idInStopOrders: 3434 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3435 3436 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3437 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3438 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3439 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3440 3441 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3442 if self.moreDebug: 3443 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3444 3445 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3446 3447 else: 3448 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3449 3450 else: 3451 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3453 def CloseAllOrders(self) -> None: 3454 """ 3455 Gets a list of open pending and stop orders and cancel it all. 3456 """ 3457 rawOrders = self.RequestPendingOrders() 3458 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3459 lenOrders = len(allOrdersIDs) 3460 3461 rawStopOrders = self.RequestStopOrders() 3462 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3463 lenSOrders = len(allStopOrdersIDs) 3464 3465 if lenOrders > 0 or lenSOrders > 0: 3466 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3467 3468 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3469 3470 else: 3471 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3473 def CloseAll(self, *args) -> None: 3474 """ 3475 Close all available (not blocked) opened trades and orders. 3476 3477 Also, you can select one or more keywords case-insensitive: 3478 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3479 3480 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3481 """ 3482 overview = self.Overview(show=False) # get all open trades info 3483 3484 if len(args) == 0: 3485 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3486 self.CloseAllOrders() # close all pending and stop orders 3487 3488 for iType in TKS_INSTRUMENTS: 3489 if iType != "Currencies": 3490 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3491 3492 else: 3493 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3494 lowerArgs = [x.lower() for x in args] 3495 3496 if "orders" in lowerArgs: 3497 self.CloseAllOrders() # close all pending and stop orders 3498 3499 for iType in TKS_INSTRUMENTS: 3500 if iType.lower() in lowerArgs and iType != "Currencies": 3501 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3503 def CloseAllByTicker(self, instrument: str) -> None: 3504 """ 3505 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3506 3507 This method searches opened trade and orders of instrument throw all portfolio and then use 3508 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3509 3510 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3511 3512 :param instrument: string with ticker. 3513 """ 3514 if instrument is None or not instrument: 3515 uLogger.error("Ticker name must be defined for using this method!") 3516 raise Exception("Ticker required") 3517 3518 overview = self.Overview(show=False) # get user portfolio with all open trades info 3519 3520 self._ticker = instrument # try to set instrument as ticker 3521 self._figi = "" 3522 3523 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3524 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3525 3526 if limitAll and self.IsInLimitOrders(portfolio=overview): 3527 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3528 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3529 3530 if stopAll and self.IsInStopOrders(portfolio=overview): 3531 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3532 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3533 3534 if self.IsInPortfolio(portfolio=overview): 3535 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3536 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3538 def CloseAllByFIGI(self, instrument: str) -> None: 3539 """ 3540 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3541 3542 This method searches opened trade and orders of instrument throw all portfolio and then use 3543 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3544 3545 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3546 3547 :param instrument: string with FIGI id. 3548 """ 3549 if instrument is None or not instrument: 3550 uLogger.error("FIGI id must be defined for using this method!") 3551 raise Exception("FIGI required") 3552 3553 overview = self.Overview(show=False) # get user portfolio with all open trades info 3554 3555 self._ticker = "" 3556 self._figi = instrument # try to set instrument as FIGI id 3557 3558 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3559 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3560 3561 if limitAll and self.IsInLimitOrders(portfolio=overview): 3562 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3563 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3564 3565 if stopAll and self.IsInStopOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if self.IsInPortfolio(portfolio=overview): 3570 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3571 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3573 @staticmethod 3574 def ParseOrderParameters(operation, **inputParameters): 3575 """ 3576 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3577 3578 :param operation: string "Buy" or "Sell". 3579 :param inputParameters: this is dict of strings that looks like this 3580 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3581 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3582 "prices" key: one or more prices to open limit-orders 3583 Counts of values in lots and prices lists must be equals! 3584 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3585 """ 3586 # TODO: update order grid work with api v2 3587 pass 3588 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3589 # 3590 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3591 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3592 # raise Exception("Incorrect value") 3593 # 3594 # if "l" in inputParameters.keys(): 3595 # inputParameters["lots"] = inputParameters.pop("l") 3596 # 3597 # if "p" in inputParameters.keys(): 3598 # inputParameters["prices"] = inputParameters.pop("p") 3599 # 3600 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3601 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3602 # raise Exception("Incorrect value") 3603 # 3604 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3605 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3606 # 3607 # if len(lots) != len(prices): 3608 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3609 # raise Exception("Incorrect value") 3610 # 3611 # uLogger.debug("Extracted parameters for orders:") 3612 # uLogger.debug("lots = {}".format(lots)) 3613 # uLogger.debug("prices = {}".format(prices)) 3614 # 3615 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3616 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3617 # uLogger.debug("Order parameters: {}".format(result)) 3618 # 3619 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3621 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3622 """ 3623 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3624 3625 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3626 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3627 """ 3628 result = False 3629 msg = "Instrument not defined!" 3630 3631 if portfolio is None or not portfolio: 3632 portfolio = self.Overview(show=False) 3633 3634 if self._ticker: 3635 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3636 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3637 3638 for iType in TKS_INSTRUMENTS: 3639 for instrument in portfolio["stat"][iType]: 3640 if instrument["ticker"] == self._ticker: 3641 result = True 3642 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3643 break 3644 3645 elif self._figi: 3646 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3647 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3648 3649 for iType in TKS_INSTRUMENTS: 3650 for instrument in portfolio["stat"][iType]: 3651 if instrument["figi"] == self._figi: 3652 result = True 3653 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3654 break 3655 3656 else: 3657 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3658 3659 uLogger.debug(msg) 3660 3661 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3663 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3664 """ 3665 Returns instrument from the user's portfolio if it presents there. 3666 Instrument must be defined by `ticker` (highly priority) or `figi`. 3667 3668 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3669 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3670 """ 3671 result = None 3672 msg = "Instrument not defined!" 3673 3674 if portfolio is None or not portfolio: 3675 portfolio = self.Overview(show=False) 3676 3677 if self._ticker: 3678 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3679 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3680 3681 for iType in TKS_INSTRUMENTS: 3682 for instrument in portfolio["stat"][iType]: 3683 if instrument["ticker"] == self._ticker: 3684 result = instrument 3685 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3686 break 3687 3688 elif self._figi: 3689 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3690 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3691 3692 for iType in TKS_INSTRUMENTS: 3693 for instrument in portfolio["stat"][iType]: 3694 if instrument["figi"] == self._figi: 3695 result = instrument 3696 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3697 break 3698 3699 else: 3700 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3701 3702 uLogger.debug(msg) 3703 3704 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3706 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3707 """ 3708 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3709 3710 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3711 3712 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3713 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3714 """ 3715 result = False 3716 msg = "Instrument not defined!" 3717 3718 if portfolio is None or not portfolio: 3719 portfolio = self.Overview(show=False) 3720 3721 if self._ticker: 3722 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3723 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3724 3725 for instrument in portfolio["stat"]["orders"]: 3726 if instrument["ticker"] == self._ticker: 3727 result = True 3728 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3729 break 3730 3731 elif self._figi: 3732 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3733 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3734 3735 for instrument in portfolio["stat"]["orders"]: 3736 if instrument["figi"] == self._figi: 3737 result = True 3738 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3739 break 3740 3741 else: 3742 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3743 3744 uLogger.debug(msg) 3745 3746 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3748 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3749 """ 3750 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3751 Instrument must be defined by `ticker` (highly priority) or `figi`. 3752 3753 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3754 3755 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3756 :return: list with `orderID`s of limit orders. 3757 """ 3758 result = [] 3759 msg = "Instrument not defined!" 3760 3761 if portfolio is None or not portfolio: 3762 portfolio = self.Overview(show=False) 3763 3764 if self._ticker: 3765 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3766 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3767 3768 for instrument in portfolio["stat"]["orders"]: 3769 if instrument["ticker"] == self._ticker: 3770 result.append(instrument["orderID"]) 3771 3772 if result: 3773 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3774 3775 elif self._figi: 3776 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3777 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3778 3779 for instrument in portfolio["stat"]["orders"]: 3780 if instrument["figi"] == self._figi: 3781 result.append(instrument["orderID"]) 3782 3783 if result: 3784 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3785 3786 else: 3787 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3788 3789 uLogger.debug(msg) 3790 3791 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3793 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3794 """ 3795 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3796 3797 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3798 3799 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3800 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3801 """ 3802 result = False 3803 msg = "Instrument not defined!" 3804 3805 if portfolio is None or not portfolio: 3806 portfolio = self.Overview(show=False) 3807 3808 if self._ticker: 3809 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3810 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3811 3812 for instrument in portfolio["stat"]["stopOrders"]: 3813 if instrument["ticker"] == self._ticker: 3814 result = True 3815 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3816 break 3817 3818 elif self._figi: 3819 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3820 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3821 3822 for instrument in portfolio["stat"]["stopOrders"]: 3823 if instrument["figi"] == self._figi: 3824 result = True 3825 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3826 break 3827 3828 else: 3829 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3830 3831 uLogger.debug(msg) 3832 3833 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3835 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3836 """ 3837 Returns list with all `orderID`s of opened stop orders for the instrument. 3838 Instrument must be defined by `ticker` (highly priority) or `figi`. 3839 3840 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3841 3842 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3843 :return: list with `orderID`s of stop orders. 3844 """ 3845 result = [] 3846 msg = "Instrument not defined!" 3847 3848 if portfolio is None or not portfolio: 3849 portfolio = self.Overview(show=False) 3850 3851 if self._ticker: 3852 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3853 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3854 3855 for instrument in portfolio["stat"]["stopOrders"]: 3856 if instrument["ticker"] == self._ticker: 3857 result.append(instrument["orderID"]) 3858 3859 if result: 3860 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3861 3862 elif self._figi: 3863 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3864 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3865 3866 for instrument in portfolio["stat"]["stopOrders"]: 3867 if instrument["figi"] == self._figi: 3868 result.append(instrument["orderID"]) 3869 3870 if result: 3871 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3872 3873 else: 3874 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3875 3876 uLogger.debug(msg) 3877 3878 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3880 def RequestLimits(self) -> dict: 3881 """ 3882 Method for obtaining the available funds for withdrawal for current `accountId`. 3883 3884 See also: 3885 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3886 - `OverviewLimits()` method 3887 3888 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3889 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3890 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3891 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3892 """ 3893 if self.accountId is None or not self.accountId: 3894 uLogger.error("Variable `accountId` must be defined for using this method!") 3895 raise Exception("Account ID required") 3896 3897 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3898 3899 self.body = str({"accountId": self.accountId}) 3900 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3901 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3902 3903 if self.moreDebug: 3904 uLogger.debug("Records about available funds for withdrawal successfully received") 3905 3906 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3908 def OverviewLimits(self, show: bool = False) -> dict: 3909 """ 3910 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3911 3912 See also: `RequestLimits()`. 3913 3914 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3915 :return: dict with raw parsed data from server and some calculated statistics about it. 3916 """ 3917 if self.accountId is None or not self.accountId: 3918 uLogger.error("Variable `accountId` must be defined for using this method!") 3919 raise Exception("Account ID required") 3920 3921 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3922 3923 view = { 3924 "rawLimits": rawLimits, 3925 "limits": { # parsed data for every currency: 3926 "money": { # this is an array of portfolio currency positions 3927 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3928 }, 3929 "blocked": { # this is an array of blocked currency 3930 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3931 }, 3932 "blockedGuarantee": { # this is locked money under collateral for futures 3933 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3934 }, 3935 }, 3936 } 3937 3938 # --- Prepare text table with limits in human-readable format: 3939 if show: 3940 info = [ 3941 "# Withdrawal limits\n\n", 3942 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3943 "* **Account ID:** [{}]\n".format(self.accountId), 3944 ] 3945 3946 if view["limits"]["money"]: 3947 info.extend([ 3948 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3949 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3950 ]) 3951 3952 else: 3953 info.append("\nNo withdrawal limits\n") 3954 3955 for curr in view["limits"]["money"].keys(): 3956 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3957 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3958 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3959 3960 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3961 "[{}]".format(curr), 3962 "{:.2f}".format(view["limits"]["money"][curr]), 3963 "{:.2f}".format(availableMoney), 3964 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3965 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3966 ) 3967 3968 if curr == "rub": 3969 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3970 3971 else: 3972 info.append(infoStr) 3973 3974 infoText = "".join(info) 3975 3976 uLogger.info(infoText) 3977 3978 if self.withdrawalLimitsFile: 3979 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3980 fH.write(infoText) 3981 3982 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3983 3984 if self.useHTMLReports: 3985 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3986 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3987 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3988 3989 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3990 3991 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3993 def RequestAccounts(self) -> dict: 3994 """ 3995 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3996 3997 See also: 3998 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3999 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4000 - `OverviewUserInfo()` method 4001 4002 :return: dict with raw data from server that contains accounts info. Example of dict: 4003 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4004 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4005 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4006 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4007 """ 4008 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4009 4010 self.body = str({}) 4011 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4012 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4013 4014 if self.moreDebug: 4015 uLogger.debug("Records about available accounts successfully received") 4016 4017 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4019 def RequestUserInfo(self) -> dict: 4020 """ 4021 Method for requesting common user's information. 4022 4023 See also: 4024 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4025 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4026 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4027 - `OverviewUserInfo()` method 4028 4029 :return: dict with raw data from server that contains user's information. Example of dict: 4030 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4031 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4032 """ 4033 uLogger.debug("Requesting common user's information. Wait, please...") 4034 4035 self.body = str({}) 4036 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4037 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4038 4039 if self.moreDebug: 4040 uLogger.debug("Records about current user successfully received") 4041 4042 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4044 def RequestMarginStatus(self, accountId: str = None) -> dict: 4045 """ 4046 Method for requesting margin calculation for defined account ID. 4047 4048 See also: 4049 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4050 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4051 - `OverviewUserInfo()` method 4052 4053 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4054 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4055 Example of responses: 4056 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4057 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4058 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4059 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4060 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4061 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4062 """ 4063 if accountId is None or not accountId: 4064 if self.accountId is None or not self.accountId: 4065 uLogger.error("Variable `accountId` must be defined for using this method!") 4066 raise Exception("Account ID required") 4067 4068 else: 4069 accountId = self.accountId # use `self.accountId` (main ID) by default 4070 4071 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4072 4073 self.body = str({"accountId": accountId}) 4074 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4075 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4076 4077 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4078 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4079 rawMargin = {} 4080 4081 else: 4082 if self.moreDebug: 4083 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4084 4085 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4087 def RequestTariffLimits(self) -> dict: 4088 """ 4089 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4090 4091 See also: 4092 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4093 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4094 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4095 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4096 - `OverviewUserInfo()` method 4097 4098 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4099 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4100 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4101 """ 4102 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4103 4104 self.body = str({}) 4105 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4106 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4107 4108 if self.moreDebug: 4109 uLogger.debug("Records with limits of current tariff successfully received") 4110 4111 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4113 def RequestBondCoupons(self, iJSON: dict) -> dict: 4114 """ 4115 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4116 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4117 All dates are in UTC timezone. 4118 4119 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4120 Documentation: 4121 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4122 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4123 4124 See also: `ExtendBondsData()`. 4125 4126 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4127 If raw iJSON is not data of bond then server returns an error [400] with message: 4128 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4129 :return: dictionary with bond payment calendar. Response example 4130 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4131 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4132 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4133 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4134 """ 4135 if iJSON["figi"] is None or not iJSON["figi"]: 4136 uLogger.error("FIGI must be defined for using this method!") 4137 raise Exception("FIGI required") 4138 4139 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4140 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4141 4142 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4143 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4144 self._figi, 4145 startDate, 4146 endDate, 4147 )) 4148 4149 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4150 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4151 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4152 4153 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4154 uLogger.warning("Instrument type is not bond!") 4155 4156 else: 4157 if self.moreDebug: 4158 uLogger.debug("Records about bond payment calendar successfully received") 4159 4160 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4162 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4163 """ 4164 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4165 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4166 coupon yields, current yields and some statistics etc. 4167 4168 WARNING! This is too long operation if a lot of bonds requested from broker server. 4169 4170 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4171 4172 :param instruments: list of strings with tickers or FIGIs. 4173 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4174 for further used by data scientists or stock analytics. 4175 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4176 In XLSX-file and Pandas DataFrame fields mean: 4177 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4178 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4179 """ 4180 if instruments is None or not instruments: 4181 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4182 raise Exception("Ticker or FIGI required") 4183 4184 if isinstance(instruments, str): 4185 instruments = [instruments] 4186 4187 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4188 4189 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4190 4191 iCount = len(uniqueInstruments) 4192 tooLong = iCount >= 20 4193 if tooLong: 4194 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4195 4196 bonds = None 4197 for i, self._figi in enumerate(uniqueInstruments): 4198 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4199 4200 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4201 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4202 rawBond = self.SearchByFIGI(requestPrice=True) 4203 4204 # Widen raw data with UTC current time (iData["actualDateTime"]): 4205 actualDate = datetime.now(tzutc()) 4206 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4207 4208 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4209 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4210 4211 # Replace some values with human-readable: 4212 iData["nominalCurrency"] = iData["nominal"]["currency"] 4213 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4214 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4215 iData["aciCurrency"] = iData["aciValue"]["currency"] 4216 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4217 iData["issueSize"] = int(iData["issueSize"]) 4218 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4219 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4220 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4221 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4222 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4223 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4224 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4225 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4226 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4227 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4228 4229 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4230 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4231 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4232 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4233 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4234 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4235 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4236 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4237 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4238 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4239 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4240 4241 # Widen raw data with calendar data from `rawCalendar` values: 4242 calendarData = [] 4243 if "events" in iData["rawCalendar"].keys(): 4244 for item in iData["rawCalendar"]["events"]: 4245 calendarData.append({ 4246 "couponDate": item["couponDate"], 4247 "couponNumber": int(item["couponNumber"]), 4248 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4249 "payCurrency": item["payOneBond"]["currency"], 4250 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4251 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4252 "couponStartDate": item["couponStartDate"], 4253 "couponEndDate": item["couponEndDate"], 4254 "couponPeriod": item["couponPeriod"], 4255 }) 4256 4257 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4258 if "maturityDate" not in iData.keys(): 4259 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4260 4261 # Widen raw data with Coupon Rate. 4262 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4263 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4264 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4265 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4266 4267 # Widen raw data with Yield to Maturity (YTM) on current date. 4268 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4269 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4270 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4271 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4272 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4273 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4274 4275 iData["calendar"] = calendarData # adds calendar at the end 4276 4277 # Remove not used data: 4278 iData.pop("uid") 4279 iData.pop("positionUid") 4280 iData.pop("currentPrice") 4281 iData.pop("rawCalendar") 4282 4283 colNames = list(iData.keys()) 4284 if bonds is None: 4285 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4286 4287 else: 4288 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4289 4290 else: 4291 uLogger.warning("Instrument is not a bond!") 4292 4293 processed = round(100 * (i + 1) / iCount, 1) 4294 if tooLong and processed % 5 == 0: 4295 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4296 4297 else: 4298 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4299 4300 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4301 4302 # Saving bonds from Pandas DataFrame to XLSX sheet: 4303 if xlsx and self.bondsXLSXFile: 4304 with pd.ExcelWriter( 4305 path=self.bondsXLSXFile, 4306 date_format=TKS_DATE_FORMAT, 4307 datetime_format=TKS_DATE_TIME_FORMAT, 4308 mode="w", 4309 ) as writer: 4310 bonds.to_excel( 4311 writer, 4312 sheet_name="Extended bonds data", 4313 index=True, 4314 encoding="UTF-8", 4315 freeze_panes=(1, 1), 4316 ) # saving as XLSX-file with freeze first row and column as headers 4317 4318 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4319 4320 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4322 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4323 """ 4324 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4325 4326 WARNING! This is too long operation if a lot of bonds requested from broker server. 4327 4328 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4329 4330 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4331 extended information about bonds: main info, current prices, bond payment calendar, 4332 coupon yields, current yields and some statistics etc. 4333 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4334 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4335 for further used by data scientists or stock analytics. 4336 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4337 """ 4338 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4339 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4340 4341 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4342 4343 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4344 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4345 calendar = None 4346 for bond in extBonds.iterrows(): 4347 for item in bond[1]["calendar"]: 4348 cData = { 4349 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4350 "couponDate": item["couponDate"], 4351 "figi": bond[1]["figi"], 4352 "ticker": bond[1]["ticker"], 4353 "name": bond[1]["name"], 4354 "couponNumber": item["couponNumber"], 4355 "payOneBond": item["payOneBond"], 4356 "payCurrency": item["payCurrency"], 4357 "couponType": item["couponType"], 4358 "couponPeriod": item["couponPeriod"], 4359 "fixDate": item["fixDate"], 4360 "couponStartDate": item["couponStartDate"], 4361 "couponEndDate": item["couponEndDate"], 4362 } 4363 4364 if calendar is None: 4365 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4366 4367 else: 4368 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4369 4370 if calendar is not None: 4371 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4372 4373 # Saving calendar from Pandas DataFrame to XLSX sheet: 4374 if xlsx: 4375 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4376 4377 with pd.ExcelWriter( 4378 path=xlsxCalendarFile, 4379 date_format=TKS_DATE_FORMAT, 4380 datetime_format=TKS_DATE_TIME_FORMAT, 4381 mode="w", 4382 ) as writer: 4383 humanReadable = calendar.copy(deep=True) 4384 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4387 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4388 humanReadable.columns = colNames # human-readable column names 4389 4390 humanReadable.to_excel( 4391 writer, 4392 sheet_name="Bond payments calendar", 4393 index=False, 4394 encoding="UTF-8", 4395 freeze_panes=(1, 2), 4396 ) # saving as XLSX-file with freeze first row and column as headers 4397 4398 del humanReadable # release df in memory 4399 4400 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4401 4402 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4404 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4405 """ 4406 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4407 Also, creates Markdown file with calendar data, `calendar.md` by default. 4408 4409 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4410 4411 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4412 extended information about bonds: main info, current prices, bond payment calendar, 4413 coupon yields, current yields and some statistics etc. 4414 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4415 :param show: if `True` then also printing bonds payment calendar to the console, 4416 otherwise save to file `calendarFile` only. `False` by default. 4417 :return: multilines text in Markdown format with bonds payment calendar as a table. 4418 """ 4419 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4420 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4421 4422 infoText = "# Bond payments calendar\n\n" 4423 4424 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4425 4426 if not (calendar is None or calendar.empty): 4427 splitLine = "| | | | | | | | | |\n" 4428 4429 info = [ 4430 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4431 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4432 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4433 ] 4434 4435 newMonth = False 4436 notOneBond = calendar["figi"].nunique() > 1 4437 for i, bond in enumerate(calendar.iterrows()): 4438 if newMonth and notOneBond: 4439 info.append(splitLine) 4440 4441 info.append( 4442 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4443 " √" if bond[1]["paid"] else " —", 4444 bond[1]["couponDate"].split("T")[0], 4445 bond[1]["figi"], 4446 bond[1]["ticker"], 4447 bond[1]["couponNumber"], 4448 "{} {}".format( 4449 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4450 bond[1]["payCurrency"], 4451 ), 4452 bond[1]["couponType"], 4453 bond[1]["couponPeriod"], 4454 bond[1]["fixDate"].split("T")[0], 4455 ) 4456 ) 4457 4458 if i < len(calendar.values) - 1: 4459 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4460 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4461 newMonth = False if curDate.month == nextDate.month else True 4462 4463 else: 4464 newMonth = False 4465 4466 infoText += "".join(info) 4467 4468 if show: 4469 uLogger.info("{}".format(infoText)) 4470 4471 if self.calendarFile is not None: 4472 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4473 fH.write(infoText) 4474 4475 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4476 4477 if self.useHTMLReports: 4478 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4479 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4480 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4481 4482 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4483 4484 else: 4485 infoText += "No data\n" 4486 4487 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4489 def OverviewAccounts(self, show: bool = False) -> dict: 4490 """ 4491 Method for parsing and show simple table with all available user accounts. 4492 4493 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4494 4495 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4496 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4497 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4498 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4499 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4500 "closed": "—", "access": "Full access" }, ...}}` 4501 """ 4502 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4503 4504 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4505 accounts = { 4506 item["id"]: { 4507 "type": TKS_ACCOUNT_TYPES[item["type"]], 4508 "name": item["name"], 4509 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4510 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4511 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4512 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4513 } for item in rawAccounts["accounts"] 4514 } 4515 4516 # Raw and parsed data with some fields replaced in "stat" section: 4517 view = { 4518 "rawAccounts": rawAccounts, 4519 "stat": accounts, 4520 } 4521 4522 # --- Prepare simple text table with only accounts data in human-readable format: 4523 if show: 4524 info = [ 4525 "# User accounts\n\n", 4526 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4527 "| Account ID | Type | Status | Name |\n", 4528 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4529 ] 4530 4531 for account in view["stat"].keys(): 4532 info.extend([ 4533 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4534 account, 4535 view["stat"][account]["type"], 4536 view["stat"][account]["status"], 4537 view["stat"][account]["name"], 4538 ) 4539 ]) 4540 4541 infoText = "".join(info) 4542 4543 uLogger.info(infoText) 4544 4545 if self.userAccountsFile: 4546 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4547 fH.write(infoText) 4548 4549 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4550 4551 if self.useHTMLReports: 4552 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4553 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4554 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4555 4556 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4557 4558 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4560 def OverviewUserInfo(self, show: bool = False) -> dict: 4561 """ 4562 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4563 4564 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4565 4566 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4567 :return: dict with raw parsed data from server and some calculated statistics about it. 4568 """ 4569 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4570 tmpTicker = self._ticker 4571 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4572 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4573 self._ticker = tmpTicker 4574 4575 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4576 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4577 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4578 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4579 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4580 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4581 4582 # This is dict with parsed common user data: 4583 userInfo = { 4584 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4585 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4586 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4587 "tariff": rawUserInfo["tariff"], 4588 } 4589 4590 # This is an array of dict with parsed margin statuses for every account IDs: 4591 margins = {} 4592 for accountId in accounts.keys(): 4593 if rawMargins[accountId]: 4594 margins[accountId] = { 4595 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4596 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4597 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4598 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4599 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4600 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4601 "missing": missing["volume"], 4602 } 4603 4604 else: 4605 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4606 4607 unary = {} # unary-connection limits 4608 for item in rawTariffLimits["unaryLimits"]: 4609 if item["limitPerMinute"] in unary.keys(): 4610 unary[item["limitPerMinute"]].extend(item["methods"]) 4611 4612 else: 4613 unary[item["limitPerMinute"]] = item["methods"] 4614 4615 stream = {} # stream-connection limits 4616 for item in rawTariffLimits["streamLimits"]: 4617 if item["limit"] in stream.keys(): 4618 stream[item["limit"]].extend(item["streams"]) 4619 4620 else: 4621 stream[item["limit"]] = item["streams"] 4622 4623 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4624 limits = { 4625 "unary": unary, 4626 "stream": stream, 4627 } 4628 4629 # Raw and parsed data as an output result: 4630 view = { 4631 "rawUserInfo": rawUserInfo, 4632 "rawAccounts": rawAccounts, 4633 "rawMargins": rawMargins, 4634 "rawTariffLimits": rawTariffLimits, 4635 "stat": { 4636 "overview": overview, 4637 "userInfo": userInfo, 4638 "accounts": accounts, 4639 "margins": margins, 4640 "limits": limits, 4641 }, 4642 } 4643 4644 # --- Prepare text table with user information in human-readable format: 4645 if show: 4646 info = [ 4647 "# Full user information\n\n", 4648 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4649 "## Common information\n\n", 4650 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4651 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4652 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4653 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4654 "\n## User accounts\n\n", 4655 ] 4656 4657 for account in view["stat"]["accounts"].keys(): 4658 info.extend([ 4659 "### ID: [{}]\n\n".format(account), 4660 "| Parameters | Values |\n", 4661 "|----------------------|--------------------------------------------------------------|\n", 4662 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4663 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4664 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4665 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4666 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4667 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4668 ]) 4669 4670 if margins[account]: 4671 info.extend([ 4672 "| Margin status: | Enabled |\n", 4673 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4674 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4675 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4676 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4677 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4678 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4679 ]) 4680 4681 else: 4682 info.append("| Margin status: | Disabled |\n\n") 4683 4684 info.extend([ 4685 "\n## Current user tariff limits\n", 4686 "\n### See also\n", 4687 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4688 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4689 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4690 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4691 "\n### Unary limits\n", 4692 ]) 4693 4694 if unary: 4695 for key, values in sorted(unary.items()): 4696 info.append("\n* Max requests per minute: {}\n".format(key)) 4697 4698 for value in values: 4699 info.append(" - {}\n".format(value)) 4700 4701 else: 4702 info.append("\nNot available\n") 4703 4704 info.append("\n### Stream limits\n") 4705 4706 if stream: 4707 for key, values in sorted(stream.items()): 4708 info.append("\n* Max stream connections: {}\n".format(key)) 4709 4710 for value in values: 4711 info.append(" - {}\n".format(value)) 4712 4713 else: 4714 info.append("\nNot available\n") 4715 4716 infoText = "".join(info) 4717 4718 uLogger.info(infoText) 4719 4720 if self.userInfoFile: 4721 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4722 fH.write(infoText) 4723 4724 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4725 4726 if self.useHTMLReports: 4727 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4728 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4729 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4730 4731 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4732 4733 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4736class Args: 4737 """ 4738 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4739 """ 4740 def __init__(self, **kwargs): 4741 self.__dict__.update(kwargs) 4742 4743 def __getattr__(self, item): 4744 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4747def ParseArgs(): 4748 """This function get and parse command line keys.""" 4749 parser = ArgumentParser() # command-line string parser 4750 4751 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4752 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4753 4754 # --- options: 4755 4756 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4757 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4758 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4759 4760 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4761 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4762 4763 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4764 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4765 4766 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4767 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4768 4769 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4770 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4771 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4772 4773 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4774 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4775 4776 # --- commands: 4777 4778 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4779 4780 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4781 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4782 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4783 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4784 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4785 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4786 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4787 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4788 4789 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4790 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4791 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4792 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4793 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4794 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4795 4796 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4797 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4798 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4799 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4800 4801 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4802 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4803 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4804 4805 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4806 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4807 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4808 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4809 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4810 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4811 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4812 4813 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4814 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4815 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4816 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4817 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4818 4819 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4820 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4821 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4822 4823 cmdArgs = parser.parse_args() 4824 return cmdArgs
This function get and parse command line keys.
4827def Main(**kwargs): 4828 """ 4829 Main function for work with TKSBrokerAPI in the console. 4830 4831 See examples: 4832 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4833 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4834 """ 4835 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4836 4837 if args.debug_level: 4838 uLogger.level = 10 # always debug level by default 4839 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4840 4841 exitCode = 0 4842 start = datetime.now(tzutc()) 4843 uLogger.debug("=-" * 50) 4844 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4845 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4846 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4847 )) 4848 4849 # trying to calculate full current version: 4850 buildVersion = __version__ 4851 try: 4852 v = version("tksbrokerapi") 4853 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4854 4855 except Exception: 4856 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4857 4858 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4859 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4860 4861 try: 4862 if args.version: 4863 print("TKSBrokerAPI {}".format(buildVersion)) 4864 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4865 4866 else: 4867 # Init class for trading with Tinkoff Broker: 4868 trader = TinkoffBrokerServer( 4869 token=args.token, 4870 accountId=args.account_id, 4871 useCache=not args.no_cache, 4872 ) 4873 4874 # --- set some options: 4875 4876 if args.more: 4877 trader.moreDebug = True 4878 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4879 4880 if args.html: 4881 trader.useHTMLReports = True 4882 4883 if args.ticker: 4884 ticker = str(args.ticker).upper() # Tickers may be upper case only 4885 4886 if ticker in trader.aliasesKeys: 4887 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4888 4889 else: 4890 trader.ticker = ticker 4891 4892 if args.figi: 4893 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4894 4895 if args.depth is not None: 4896 trader.depth = args.depth 4897 4898 # --- do one command: 4899 4900 if args.list: 4901 if args.output is not None: 4902 trader.instrumentsFile = args.output 4903 4904 trader.ShowInstrumentsInfo(show=True) 4905 4906 elif args.list_xlsx: 4907 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4908 4909 elif args.bonds_xlsx is not None: 4910 if args.output is not None: 4911 trader.bondsXLSXFile = args.output 4912 4913 if len(args.bonds_xlsx) == 0: 4914 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4915 4916 else: 4917 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4918 4919 elif args.search: 4920 if args.output is not None: 4921 trader.searchResultsFile = args.output 4922 4923 trader.SearchInstruments(pattern=args.search[0], show=True) 4924 4925 elif args.info: 4926 if not (args.ticker or args.figi): 4927 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4928 raise Exception("Ticker or FIGI required") 4929 4930 if args.output is not None: 4931 trader.infoFile = args.output 4932 4933 if args.ticker: 4934 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4935 4936 else: 4937 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4938 4939 elif args.calendar is not None: 4940 if args.output is not None: 4941 trader.calendarFile = args.output 4942 4943 if len(args.calendar) == 0: 4944 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4945 4946 else: 4947 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4948 4949 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4950 4951 elif args.price: 4952 if not (args.ticker or args.figi): 4953 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4954 raise Exception("Ticker or FIGI required") 4955 4956 trader.GetCurrentPrices(show=True) 4957 4958 elif args.prices is not None: 4959 if args.output is not None: 4960 trader.pricesFile = args.output 4961 4962 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4963 4964 elif args.overview: 4965 if args.output is not None: 4966 trader.overviewFile = args.output 4967 4968 trader.Overview(show=True, details="full") 4969 4970 elif args.overview_digest: 4971 if args.output is not None: 4972 trader.overviewDigestFile = args.output 4973 4974 trader.Overview(show=True, details="digest") 4975 4976 elif args.overview_positions: 4977 if args.output is not None: 4978 trader.overviewPositionsFile = args.output 4979 4980 trader.Overview(show=True, details="positions") 4981 4982 elif args.overview_orders: 4983 if args.output is not None: 4984 trader.overviewOrdersFile = args.output 4985 4986 trader.Overview(show=True, details="orders") 4987 4988 elif args.overview_analytics: 4989 if args.output is not None: 4990 trader.overviewAnalyticsFile = args.output 4991 4992 trader.Overview(show=True, details="analytics") 4993 4994 elif args.overview_calendar: 4995 if args.output is not None: 4996 trader.overviewAnalyticsFile = args.output 4997 4998 trader.Overview(show=True, details="calendar") 4999 5000 elif args.deals is not None: 5001 if args.output is not None: 5002 trader.reportFile = args.output 5003 5004 if 0 <= len(args.deals) < 3: 5005 trader.Deals( 5006 start=args.deals[0] if len(args.deals) >= 1 else None, 5007 end=args.deals[1] if len(args.deals) == 2 else None, 5008 show=True, # Always show deals report in console 5009 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5010 ) 5011 5012 else: 5013 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5014 raise Exception("Incorrect value") 5015 5016 elif args.history is not None: 5017 if args.output is not None: 5018 trader.historyFile = args.output 5019 5020 if 0 <= len(args.history) < 3: 5021 dataReceived = trader.History( 5022 start=args.history[0] if len(args.history) >= 1 else None, 5023 end=args.history[1] if len(args.history) == 2 else None, 5024 interval="hour" if args.interval is None or not args.interval else args.interval, 5025 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5026 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5027 show=True, # shows all downloaded candles in console 5028 ) 5029 5030 if args.render_chart is not None and dataReceived is not None: 5031 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5032 5033 trader.ShowHistoryChart( 5034 candles=dataReceived, 5035 interact=iChart, 5036 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5037 ) 5038 5039 else: 5040 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5041 raise Exception("Incorrect value") 5042 5043 elif args.load_history is not None: 5044 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5045 5046 if args.render_chart is not None and histData is not None: 5047 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5048 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5049 5050 trader.ShowHistoryChart( 5051 candles=histData, 5052 interact=iChart, 5053 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5054 ) 5055 5056 elif args.trade is not None: 5057 if 1 <= len(args.trade) <= 5: 5058 trader.Trade( 5059 operation=args.trade[0], 5060 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5061 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5062 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5063 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5064 ) 5065 5066 else: 5067 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5068 5069 elif args.buy is not None: 5070 if 0 <= len(args.buy) <= 4: 5071 trader.Buy( 5072 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5073 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5074 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5075 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5076 ) 5077 5078 else: 5079 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5080 5081 elif args.sell is not None: 5082 if 0 <= len(args.sell) <= 4: 5083 trader.Sell( 5084 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5085 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5086 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5087 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5088 ) 5089 5090 else: 5091 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5092 5093 elif args.order: 5094 if 4 <= len(args.order) <= 7: 5095 trader.Order( 5096 operation=args.order[0], 5097 orderType=args.order[1], 5098 lots=int(args.order[2]), 5099 targetPrice=float(args.order[3]), 5100 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5101 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5102 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5103 ) 5104 5105 else: 5106 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5107 5108 elif args.buy_limit: 5109 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5110 5111 elif args.sell_limit: 5112 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5113 5114 elif args.buy_stop: 5115 if 2 <= len(args.buy_stop) <= 7: 5116 trader.BuyStop( 5117 lots=int(args.buy_stop[0]), 5118 targetPrice=float(args.buy_stop[1]), 5119 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5120 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5121 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5122 ) 5123 5124 else: 5125 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5126 5127 elif args.sell_stop: 5128 if 2 <= len(args.sell_stop) <= 7: 5129 trader.SellStop( 5130 lots=int(args.sell_stop[0]), 5131 targetPrice=float(args.sell_stop[1]), 5132 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5133 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5134 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5135 ) 5136 5137 else: 5138 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5139 5140 # elif args.buy_order_grid is not None: 5141 # # update order grid work with api v2 5142 # if len(args.buy_order_grid) == 2: 5143 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5144 # 5145 # for order in orderParams: 5146 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5147 # 5148 # else: 5149 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5150 # 5151 # elif args.sell_order_grid is not None: 5152 # # update order grid work with api v2 5153 # if len(args.sell_order_grid) >= 2: 5154 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5155 # 5156 # for order in orderParams: 5157 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5158 # 5159 # else: 5160 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5161 5162 elif args.close_order is not None: 5163 trader.CloseOrders(args.close_order) # close only one order 5164 5165 elif args.close_orders is not None: 5166 trader.CloseOrders(args.close_orders) # close list of orders 5167 5168 elif args.close_trade: 5169 if not (args.ticker or args.figi): 5170 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5171 raise Exception("Ticker or FIGI required") 5172 5173 if args.ticker: 5174 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5175 5176 else: 5177 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5178 5179 elif args.close_trades is not None: 5180 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5181 5182 elif args.close_all is not None: 5183 if args.ticker: 5184 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5185 5186 elif args.figi: 5187 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5188 5189 else: 5190 trader.CloseAll(*args.close_all) 5191 5192 elif args.limits: 5193 if args.output is not None: 5194 trader.withdrawalLimitsFile = args.output 5195 5196 trader.OverviewLimits(show=True) 5197 5198 elif args.user_info: 5199 if args.output is not None: 5200 trader.userInfoFile = args.output 5201 5202 trader.OverviewUserInfo(show=True) 5203 5204 elif args.account: 5205 if args.output is not None: 5206 trader.userAccountsFile = args.output 5207 5208 trader.OverviewAccounts(show=True) 5209 5210 else: 5211 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5212 raise Exception("There is no command to execute") 5213 5214 except Exception: 5215 trace = tb.format_exc() 5216 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5217 if e in trace: 5218 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5219 break 5220 5221 uLogger.debug(trace) 5222 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5223 exitCode = 255 # an error occurred, must be open a ticket for this issue 5224 5225 finally: 5226 finish = datetime.now(tzutc()) 5227 5228 if exitCode == 0: 5229 if args.more: 5230 uLogger.debug("All operations were finished success (summary code is 0).") 5231 5232 else: 5233 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5234 os.path.abspath(uLog.defaultLogFile), exitCode, 5235 )) 5236 5237 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5238 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5239 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5240 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5241 )) 5242 uLogger.debug("=-" * 50) 5243 5244 if not kwargs: 5245 sys.exit(exitCode) 5246 5247 else: 5248 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: